Get started
Browse docs
On this page
  1. What QuoxVault is
  2. Credential kinds
  3. Envelope encryption
  4. Dynamic leasing
  5. Tamper-evident audit
  6. Cross-org isolation
  7. Stable, beta and planned
  8. Test coverage
  9. Operating notes

QuoxVault

QuoxVault is the credential vault for AI agents. It stores your organisation's real secrets, typed and encrypted, and grants agents short-lived, scoped leases to use them server-side, so the secret itself never reaches the agent. Every release is recorded on a tamper-evident audit chain.

Part of QuoxCORE · maturity map last audited 2026-07-11

What QuoxVault is #

Most agent stacks hand the model an API key and hope. QuoxVault takes the opposite position: secrets stay in the vault, and agents are granted leases, time-boxed, use-counted permissions to have a secret used on their behalf.

The credential is resolved and consumed server-side by a typed executor (send an email, run an SSH command, query a database), and the plaintext never appears in the agent's context, its logs or its reply.

Three ideas carry the design:

  • Typed storage. A secret is not just a string. Each credential kind carries shape validation and its own release policy.
  • Leased release. Access is a lease with an expiry and an optional use count, requested by an agent and approvable by a human.
  • Witnessed access. Every release emits an audit event witnessed onto the WARD hash chain, so the record of who accessed what is tamper-evident.

QuoxVault ships today as a capability inside QuoxCORE's auth service. A standalone vault product is planned and nothing of it has shipped; see stable, beta and planned.

The QuoxVault console in QuoxCORE, showing stored credentials and active leases

Credential kinds Stable #

The taxonomy defines eight credential kinds. Each kind declares the shape a credential must have and the release policy that governs how it may leave the vault. Shape validation runs on the decrypted fields, and there is deliberately no database constraint on the kind column, so a new kind can ship without a migration.

KindHoldsConsumer todayMaturity
service_apiProvider API keysResolver waterfall: bring-your-own-key, then org, instance and environment keysStable
email_accountIMAP and SMTP accountsemail_read and email_send tools. Username and password only; OAuth is not shippedBeta
web_loginSite logins, TOTP seedsGoverned browser login; TOTP codes are computed server-side and the seed never leaves the serverBeta
ssh_keySSH private keysremote_ssh: key used in memory only, host keys pinned on first use, drift hard-failsBeta
tls_certCertificates and keystls_cert_info: X.509 parse, expiry and metadataBeta
databaseDatabase credentialsdb_query: read-only by default, writes need an explicit flagBeta
cloudAWS, GCP, Azure credentialsNone. Stores and decrypts correctly, but no executor opens a cloud session yetStub
genericAnything elseStorage and governed release onlyStable

Alongside the eight kinds, encrypted session state (cookies and session tokens for browser automation) has its own store with a 12 hour staleness contract, replacing an earlier plaintext cookie-file design. Stable

NoteGmail, Outlook and Microsoft 365 OAuth do not exist in QuoxVault today. The accepted credential shape reserves fields for OAuth tokens, but there is no consent flow or refresh-token exchange. IMAP and SMTP with a username and password is the only working email path.

Envelope encryption Stable #

Every credential is sealed in its own envelope. A key encryption key (KEK) is derived from the master key with HKDF; each credential gets its own data encryption key (DEK), which encrypts the secret with AES-256-GCM and is itself wrapped by the KEK.

The GCM authentication tag means any bit flip in the ciphertext or the wrapped DEK makes decryption throw rather than return corrupt plaintext. Legacy rows in the older three-part format still decrypt through the same router.

on-disk formats
# two formats, one decrypt router (dispatch on the v2: prefix)
legacy   iv:authTag:ciphertext          # v1 rows still decrypt
v2       v2:<envelope>                  # per-credential DEK, wrapped by the KEK

Rotation is a re-wrap

Rotating the master key re-wraps only the DEKs, not the ciphertexts. SettingMASTER_ENCRYPTION_KEY_PREVIOUS opens a dual-key window in which rows wrapped under the previous key still decrypt, so rotation cannot lock an operator out.

Coverage by construction

A registry lists every table that holds vault ciphertext (six today). A CI test fails if a new ciphertext column ships without being registered, so rotation and key validation cannot silently miss a store. This closed a real incident class: an earlier rotation missed a table because coverage relied on discipline rather than a failing test.

Known gapThe KEK is global, not per organisation. Tenant isolation of encrypted data is an application-layer property (org-scoped routes and queries), not a cryptographic one: anyone holding the master key and the salt can unwrap every organisation's DEKs. This is a recorded decision (2026-07-11), accepted while the platform is effectively single-tenant, to be revisited when a second real tenant lands or the standalone vault is scheduled.

NoteDEK buffers are not yet zeroed after use, a cheap follow-up that has not landed. Decrypted values held as JavaScript strings cannot be scrubbed from memory at all; that is a Node and V8 platform limitation, accepted and documented rather than claimed away.

Dynamic leasing Beta #

A lease binds one agent to one credential for a bounded window: an expiry, and optionally a maximum number of uses. The lease lifecycle (create, approve, revoke, expire on a 30 second tick) is Stable and predates the current audit.

The enforcement wiring that makes live release paths respect leases is Beta: it was live-verified on 2026-07-11, with enforced mode returning 403 without a lease and a five-use lease allowing exactly five releases.

Enforcement is one fail-closed check in a single SQL predicate, so the three conditions cannot drift apart, and use counting is atomic. A lease held by the wrong agent is a WHERE-clause miss, not a comparison that could be bypassed.

the lease gate predicate
-- a required lease releases only when all three hold, in one predicate
status = 'active'
  AND expires_at > now()
  AND (max_uses IS NULL OR use_count < max_uses)

Enforcement modes

Each organisation carries a three-state flag: off, advisory orenforced. The default is advisory, a deliberate rollout choice, and a malformed setting fails safe rather than silently becoming enforced.

WarningIn advisory mode, the default for every organisation, a caller that omits a lease still receives the secret. The audit trail is the only defence: detective, not preventive. There is currently no dedicated UI or API to flip an organisation to enforced; the only write path is the generic organisation settings endpoint.

And off drops the lease audit events as well as enforcement, a silent double loss. If you rely on leases as a control, set the mode to enforced and verify it.

Human approval

A pending lease request creates an item in the HITL inbox instead of timing out silently. Approving the item resolves the lease. Live-verified 2026-07-11.Beta

Tamper-evident audit Beta #

Every secret release path emits a secret.access audit event and witnesses it onto the WARD hash chain. Verifying the chain detects mutation of any witnessed entry: change a row and the hash linkage breaks, and verification reports it.

Honesty requires the history here. The 2026-07 adversarial audit found that an intent-prefix mismatch had caused every witness call since the integration shipped to silently write zero chain entries; the call never threw, and the only existing test checked that it did not throw.

The bug was fixed in the same audit and 15 chain entries were live-verified afterwards. The audit also found that the witnessed hash did not commit to the actor, target and organisation fields of the underlying event; the stream's decisions log records that gap closed later in the same audit with a reconciliation check and release audit on all resolve paths.

  • Chain tamper detection: mutating a chain row is detected and reported as broken. Stable, narrow scope.
  • Witnessing on secret access: wired on all release paths, live-verified after the fix above. Beta

Known gapPlugin backend credential injection bypasses the resolver, the lease gate and the audit chain entirely: the supervisor decrypts and injects plaintext as container environment variables, visible to anyone who can inspect the container. Fixing it breaks the plugin SDK credential contract, so it is deferred by recorded decision to a tracked stream rather than patched quietly. Treat plugin-injected credentials as outside the vault's guarantees today.

Cross-org isolation Stable #

Every internal release endpoint checks the caller's organisation against the credential's, and mismatches return 404 rather than 403 where an existence oracle would leak that a credential id is real. This isolation is application-layer; the cryptographic caveat is the global KEK described under envelope encryption.

Internal release pathOrg check
integrations/:id/decryptMandatory, 403 on mismatch
integrations/:id/service-decryptMandatory, 404 on mismatch, no existence oracle
integrations/by-domain/:domainMandatory, queries scoped to the org
credential-decrypt/:idMandatory, 404 on mismatch
credential-totp/:idMandatory, 404 on mismatch
resolve-for-agentOrg and agent scope the full resolution chain
resolve-by-idOrg id required in all environments; requests without it are rejected

The resolve-by-id path was the last one to reach mandatory enforcement: for a period it verified the org id only when supplied, and requiring it in production waited on one caller being migrated to thread the org id through. That migration landed and the requirement is now on everywhere; only an explicit opt-out environment variable disables it.

Stable, beta and planned #

This section mirrors the surface's status and limits document, the honest maturity map kept with the code and updated at every audit. If the two ever disagree, the document in the repository wins.

Stable Verified and pinned by regression tests

  • v2 envelope encryption (HKDF KEK, per-credential DEK, AES-256-GCM), with legacy rows decrypting through the same router
  • Key rotation as a DEK re-wrap with a dual-key window
  • Encrypted-store completeness enforced by a failing CI test
  • Boot-time master key validation and the periodic key health scan
  • The typed credential taxonomy and its shape validation
  • Encrypted session state with a 12 hour staleness contract
  • Lease lifecycle: create, approve, revoke, expiry job
  • Org checks on every internal release endpoint
  • WARD chain tamper detection over witnessed entries

Beta Working, live-verified once, still hardening

  • Email consumer (IMAP read, SMTP send)
  • Web login consumer with server-side TOTP
  • SSH consumer with trust-on-first-use host key pinning
  • TLS certificate parsing and expiry metadata
  • Database query consumer, read-only by default
  • Lease enforcement wiring and the three-state org mode
  • HITL inbox approval for lease requests
  • WARD witnessing of every secret access
  • Typed field widgets in the credential UI

Planned Designed or reserved, nothing shipped

  • Gmail, Outlook and Microsoft 365 OAuth email credentials
  • Executors for the cloud credential kind
  • An egress guard on vault-fed host fields, to be wired when those executors ship
  • The standalone QuoxVault product outside QuoxCORE

Known gap Documented limitations, not softened

  • Plugin backend credential injection bypasses the resolver, lease gate and audit chain; deferred by recorded decision to a tracked stream
  • The KEK is global rather than per organisation; encrypted-at-rest tenant isolation is application-layer only
  • Lease enforcement defaults to advisory, and no dedicated UI or API flips an organisation to enforced
  • off mode drops lease audit events along with enforcement
  • DEK buffers are not zeroed after use, and decrypted strings cannot be scrubbed in Node
  • Backup bundles contain vault ciphertexts; a backup plus the master key decrypts everything in it (accepted and documented)
  • The suite is not yet self-red-teamed against a live deployment; container log sweeps and a staging key rotation drill remain open

Test coverage #

The vault's claims are backed by an adversarial test corpus that runs against real database rows and real service boundaries, not mocks of the vault itself. Roughly 140 tests across eight files:

CorpusTestsCovers
Adversarial suite54Cross-org isolation on every release path, forged service keys, lease abuse (expired, revoked, over-limit, wrong agent), chain tampering, envelope round-trips, TOTP vector correctness
Log-leak greps23No passwords, private keys or PEM material in captured logs across representative flows
Integration encryption boundary22Encryption behaviour at service boundaries
Envelope unit tests11Round-trip, bit-flip tamper evidence, legacy dispatch
Credential decrypt regression10The decrypt endpoint returns plaintext, not echoed ciphertext
Collector field encryption10Collector-side encryption boundaries
Security pins7Closed findings pinned green as regression guards
Store completeness3CI fails on an unregistered ciphertext column

Stated plainly, what this corpus does not verify: a live red-team exercise against a running deployment, end-to-end container log sweeps across the full stack, a staging rehearsal of master key rotation, and real IMAP or SSH endpoints (transports are mocked).

Four tests are explicitly marked as findings rather than passes: they assert that hostile server-shaped values are stored and returned unchanged, not that a guard exists, because none does yet.

Operating notes #

The settings an operator actually touches:

environment
MASTER_ENCRYPTION_KEY=<32-byte base64>   # required; boot fails loudly without it
MASTER_ENCRYPTION_KEY_PREVIOUS=           # set only during a rotation window
RESOLVE_BY_ID_REQUIRE_ORG=                # unset means enforced; 'false' opts out
SettingDefaultEffect
MASTER_ENCRYPTION_KEYnoneThe raw master key. Validated at boot; an empty value fails loudly at the application layer
MASTER_ENCRYPTION_KEY_PREVIOUSunsetOpens the dual-key rotation window. Leaving it set indefinitely keeps a leaked previous key as a live decrypt path
vault.lease_enforcement (per org)advisoryoff, advisory or enforced. Fails safe on a malformed value
RESOLVE_BY_ID_REQUIRE_ORGunset, meaning enforcedOrg id is required on resolve-by-id in all environments; only an explicit false disables the check

WarningCompose files interpolate an unset MASTER_ENCRYPTION_KEY to an empty string without complaint; the boot-time check is what catches it. Do not remove that check, and do not run the vault with enforcement assumptions you have not verified against your organisation's actual mode.

Sourced from the QuoxVault status and limits document, audited 2026-07-11← Back to docs