How the Versus Vault protects secrets at rest and in use — per-secret envelope encryption with AES-256-GCM, a rotatable master key, fail-closed key checks at boot, a tamper-evident hash-chained audit, scoped app-principal access, and dual-control break-glass reveals.
The Versus Vault is where the platform keeps the credentials that make automation possible — device logins, API tokens, certificate keys. Because those secrets are the crown jewels, the vault is hardened along several independent axes: how a value is encrypted at rest, whether the system will even start with a weak key, who can read a secret and how that read is proven after the fact, and how a human breaks glass when a stored value must actually be seen. This page explains each of those, at the level a security reviewer needs.
None of this changes how you use the vault day to day — you still write and read secrets by path. Hardening is what happens underneath.
Every secret value is sealed with envelope encryption rather than a single shared key. When you write a version, the vault:
Because every secret gets its own DEK, compromising one sealed value never exposes another — there is no shared per-record key to unravel the whole store. And because AES-GCM is an authenticated cipher, the seal is tamper-evident: any bit-flip in the stored ciphertext or the wrapped DEK fails authentication on decrypt rather than silently returning garbage.
The sealed token is versioned with a small header (a magic marker plus a kek_ref tag) so its format can evolve without ambiguity:
magic 'VV1' version marker
kek_ref which master KEK sealed this blob
dek_wrapped nonce ‖ AES-256-GCM(KEK, DEK)
value_wrapped nonce ‖ AES-256-GCM(DEK, plaintext)
Each secret version stores this token in a dedicated sealed column. A legacy single-key column is retained read-only so that values written before envelope encryption can still be decrypted, but every new write goes through the envelope path and never persists plaintext under the old scheme.
The KEK that wraps every DEK is resolved in one of two ways:
| Source | When it's used |
|---|---|
Explicit VAULT_KEK | A 256-bit key supplied as base64 in the environment. Preferred for production — escrow it like any root key. |
Derived from SECRET_KEY | If no explicit KEK is set, one is derived via PBKDF2-HMAC-SHA256 (200,000 iterations) with a vault-specific salt. |
The derivation salt is vault-specific, so the vault KEK is distinct from the platform's field-encryption key and from other subsystems' keys — one derived key is never reused across purposes.
The kek_ref tag inside each sealed blob records which KEK sealed it. That indirection is what makes the master key rotatable: to roll the KEK you re-wrap the DEKs (cheap — only the small wrapped-DEK portion changes) without ever touching or re-encrypting the value ciphertext. It also leaves room to introduce an HSM- or KMS-backed KEK later without a storage-format change.
The committed code ships with development-default values for SECRET_KEY and the field-encryption key so a laptop checkout runs out of the box. Those defaults are dangerous in production — a known SECRET_KEY makes the derived vault KEK predictable, and a known field key makes encrypted fields world-decryptable.
So the platform refuses to boot in any non-debug deployment if either key is still the built-in default:
ImproperlyConfigured: Refusing to start: the following cryptographic key(s)
are still the built-in development default(s): SECRET_KEY. Set them to strong,
unique values (and escrow them).
This is a deliberate fail-closed posture: a misconfigured instance never comes up in a state where secrets could be trivially decrypted. The check can be waived only for throwaway environments, via an explicit VERSUS_ALLOW_DEFAULT_KEYS=1 opt-out — you have to say, in the environment, that this instance is disposable.
Every vault operation — write, read, delete, lease, rotate, reveal, enroll — is recorded twice: once in the platform's flat audit log, and once in a dedicated hash-chained vault ledger.
In the chained ledger, each event carries a monotonic sequence number and a hash computed over its payload and its predecessor's hash:
hash = SHA-256( seq · prev_hash · { actor, action, mount, path, details, at } )
Because each row commits to the one before it, any edit or deletion of a past row breaks the chain from that point forward. An integrity walk re-derives the chain and reports exactly where, if anywhere, it was broken:
{ "events": 10423, "intact": true, "broken_at": null }
An intact: false with a broken_at sequence number is a signal that historical audit rows were altered or removed after the fact — something a flat log alone cannot prove.
Access to the vault is capability-based, evaluated per path. A policy grants a role a set of capabilities on a path glob:
| Capability | Grants |
|---|---|
read | Read a secret's value. |
list | Enumerate paths under a prefix. |
write | Create or update a version. |
delete | Soft-delete a secret. |
manage | Destroy, force-release, and other administrative actions. |
checkout | Exclusively check a credential out for a bounded window. |
rotate | Trigger rotation of a secret. |
reveal | Open a break-glass reveal request (see below). |
approve | Approve someone else's reveal request. |
session | Open a brokered session against a credential without ever seeing it. |
The session capability is deliberately disjoint from read — it lets Privileged Session Management inject a credential into an SSH or HTTPS session that the operator never sees, so "use it" and "see it" are separately grantable.
Path policies can also carve out per-user namespaces. A single policy whose glob contains the ${user} token — for example personal/${user}/** — is substituted with the requesting user's username before matching, so one rule gives every user an isolated personal area and never matches on an anonymous or service call.
Automation reads secrets through app principals, not human logins. An app principal authenticates with client credentials and receives a short-lived bearer token:
POST /api/vault/token
Content-Type: application/json
{ "client_id": "svc-collector", "client_secret": "••••••••" }
200 OK
{ "access_token": "…", "token_type": "Bearer", "expires_in": 900 }
A machine then fetches a secret by reference, presenting that bearer token:
GET /api/vault/fetch?ref=vault://root/devices/edge-fw-1/credential&field=password
Authorization: Bearer <app-principal token>
Several hardening rules apply to this path specifically:
client_id can't be brute-forced against.checkout, reveal, or approve — the capabilities that model a human in the loop.allowed_paths glob that scopes each principal to exactly what it needs.This is the same trust boundary the collectors rely on when they resolve device credentials — see Collector Vault Modes for how a credential can be resolved locally on the collector so it never leaves your network.
Sometimes a human genuinely must see a stored value — a last-resort recovery, an out-of-band login. That path is deliberately high-friction and dual-controlled:
reveal opens a reveal request against the secret, with a reason. This alone reveals nothing.approve approves it. Self-approval is rejected — the approver cannot be the requester. Approval issues a one-time token valid for five minutes.Every step in that sequence is written to the audit chain, so a reveal is always attributable to a named requester and a named approver.
Hardening isn't only about protecting a secret in place — it's also about shrinking the window a leaked secret is useful. Rotation is two-phase and verify-first:
A scheduled sweep rotates secrets whose policy interval has elapsed, and defers any secret that is actively checked out until it's checked back in, so rotation never yanks a credential out from under a running job. Rotation-on-reveal and rotation-on-checkin let you tie a rotation to the exact moments a secret was exposed.
Vault hardening is one layer of the platform's defense-in-depth, alongside row-level tenant isolation, the outbound-only collector plane, and OIDC SSO with mandatory MFA. Secrets stay envelope-encrypted at rest, unreadable to a machine outside its scoped principal, un-seeable to a human without a second approver, and every touch is provable after the fact. For an end-to-end narrative of a single secret moving through these controls, see A Day in the Life of a Secret.
session capability: use a credential without seeing it.