| Control | Where | Notes |
|---|---|---|
| Encryption at rest | API + KMS | XChaCha20-Poly1305 over a KMS-wrapped DEK; only ciphertext on disk. |
| Key management | AWS KMS | Master key never leaves KMS; DEKs are per-version and zeroized after use. |
| Read authorization | API | IPv4/CIDR allowlist, fail-closed. |
| Management authorization | API | Admin bearer tokens (SHA-256 fingerprints, constant-time check) + RBAC. |
| Tamper resistance | core | AEAD authentication tag; AAD binds ciphertext to name:version. |
| Replication integrity | raft | Writes are committed via Raft consensus to a quorum before they are acknowledged. |
| Auditing | API | Per-node audit_log records actor, client IP, action, outcome. |
| Memory hygiene | core | Zeroizing for DEKs and master keys. |
| KMS-outage resilience | API | In-memory unwrapped-DEK cache (VAULT_DEK_CACHE_TTL_SECS); plaintext is never cached. |
- Production uses
VAULT_KMS_MODE=awswithVAULT_KMS_KEY_ID. Grant the API's IAM principal onlykms:GenerateDataKeyandkms:Decrypton that key (addkms:ListKeyRotationsandkms:ReEncryptFrom/kms:ReEncryptToif the re-wrap worker below is enabled). AWS credentials and region come from the standard AWS SDK environment, not fromVAULT_*variables. localmode (VAULT_LOCAL_MASTER_KEY, base64 32 bytes) is for development and tests only. Without the env var a random ephemeral master key is used and all secrets become undecryptable on restart — by design, to make misuse obvious. In a cluster, every node must share the same master key.- DEKs are generated per secret version, used once to seal, and zeroized.
- Automatic CMK rotation is transparent. Each version stores the
kms_key_idthat wrapped its DEK; decrypt uses that stored id, not the configured one. AWS retains old key material under the same ARN, so existing versions keep decrypting after a rotation with no action required. - Re-wrap worker (optional). When
VAULT_KMS_REWRAP_ENABLED=true, a leader-gated worker pollskms:ListKeyRotations(everyVAULT_KMS_REWRAP_POLL_SECS, default daily). When it sees a newer rotation than the persisted watermark itReEncrypts every live version's wrapped DEK onto the current CMK material — the plaintext DEK never leaves KMS and the ciphertext/nonce/AAD are untouched. Progress is tracked per version (wrapped_rotation_at) so a pass is resumable and idempotent; pacing is bounded byVAULT_KMS_REWRAP_MAX_PER_SEC. POST /v1/admin/kms/rewrapforces a pass;GET /v1/admin/kms/rewrap/statusreports pending versions. Both require an admin token.- Rollout: the worker introduces new Raft commands. Deploy the release with
VAULT_KMS_REWRAP_ENABLED=false, roll every node, then enable it — so a new command is never replicated to a node that cannot decode it. Enabling adopts the current rotation as the baseline without an initial sweep; the first re-wrap happens on the next rotation (or a manual force).
- The built-in
adminrole is seeded on first start. To mint the first admin token, start the node(s) withVAULT_BOOTSTRAP_TOKEN=<token>(the same value on every node); it creates abootstrap-admintoken mapped toadmin. Use it to create real per-service tokens viaPOST /v1/tokens, then rotate it. - Management (
create/update/delete/rotate, role/token admin) requires a bearer token whose role grants the action on the secret path. Reads are governed by the IP allowlist, not RBAC. Access invariants are enforced in the API process; the storage layer holds only ciphertext and is reachable only through it.
- Raft RPC between nodes is plain HTTP on the internal Raft port. Run the
cluster on a private network; the WireGuard overlay
(
WIREGUARD.md) is the supported way to keep both the Raft and admin surfaces off the public internet. - API: terminate TLS at the API or a trusted local proxy. If a proxy sets
the client IP, enable
VAULT_TRUST_PROXY=trueonly when the proxy is trusted and strips inboundX-Forwarded-For; otherwise the allowlist can be spoofed.
Tokens are 256-bit random strings; only sha256(token) is stored, compared in
constant time. Issue them via POST /v1/tokens (the raw token is returned once),
distribute over a secure channel, and revoke via DELETE /v1/tokens/{name}
(sets revoked_at). The plaintext token is never persisted.