Where do audit keys live?¶
Audit uses two kinds of key. Pseudonymisation keys are off by default. Signing keys sign seals and are independent of pseudonyms.
| kind | does | how many | held by | ends by |
|---|---|---|---|---|
| pseudonymisation (symmetric) | turns an identifier into a stable pseudonym and seals the identity behind it | one per tenant and purpose, created on first use | the writer makes, resolve opens | destruction, which is erasure |
| signing (asymmetric) | signs seals | one per installation | the notary only, never the writer | replacement, with the old public half kept |
Pseudonymisation keys¶
No keys is the default (keys unset): no key directory, no identity/ prefix, and resolve refused as unimplemented. Such a deployment declares external_identifiers_are_opaque and skips the rest of this section.
Choose a provider only if the contract demands erasure of an identifier from a locked archive. Moving keys later gives every person a new identity.
The model is one key per tenant and purpose, where a purpose is a profile name. Two purposes yield unrelated pseudonyms for one person.
pseudonym = "ps_" + base64url( HMAC-SHA256( key[tenant, purpose], identifier ) )
For resolve, the writer also seals the identifier: under the tenant's own key with transit, under the conceal key with kms and local. The sealed form lands at identity/tenant=<t>/purpose=<p>/<pseudonym>. Set forgetIdentities: true to turn this off.
| provider | where the key lives | use it for |
|---|---|---|
none |
no keys | anything that need not crypto-shred |
local |
32-byte data keys wrapped under a 32-byte root in a Secret, kept by the local adapter | tests, a laptop, one instance |
transit |
one key per profile and tenant, named <key>.pseudonym.<tenant>, inside an OpenBAO or Vault transit engine |
more than one replica, or secrets already in OpenBAO |
kms (keys.adapter) |
data keys from GenerateDataKey under one customer-managed KMS key, wrapped copies in the index database or SSM (keys.state) |
AWS without OpenBAO |
With local, set the root with keys.rootFile; the adapter is for tests and a single instance.
With transit, the writer creates each tenant's key on first use and signs in with the pod's projected token on a JWT auth mount. Policy scopes each role:
- The writer may
hmacandencrypton its profiles' keys, and touch nothing that rotates, configures or trims a key. - The resolve role may
decryptand nothing else. - Only a human eraser role reaches
rotate,configandtrim.
OpenBAO keys has the engine set-up.
With kms, set keys.adapter: kms and keys.state: backend: database keeps wrapped secrets in your PostgreSQL, backend: ssm keeps them under root. Without keys.state the writer cannot pseudonymise.
Never rotated¶
Rotating a pseudonymisation key gives every person a second, unrelated pseudonym.
Destroying a key¶
audit key destroy --tenant <t> --purpose <p> --by <who> --reason <why> is erasure. It refuses while a legal hold covers the tenant. It refuses without a writer to record audit.key.destroyed.
The pseudonyms stay in the archive but nothing can compute or match them. The provider leaves a tombstone, checked before any key is created, so a later record cannot mint a fresh key. With transit every sealed identity is unreadable. With kms and local the provider refuses to open it, but the conceal key still decrypts the stored copy.
With transit, destroy rotates the tenant's key once, raises the minimum usable version past the first and trims the first version. The key stays as the tombstone. A tenant destroyed before it was seen gets a key made and destroyed at once.
Signing key¶
The notary (audit-notary) signs each hourly seal ES384 with a P-384 key. The writer must have no path to that key. The chart refuses a notary that shares the writer's ServiceAccount, cloud role annotations or OpenBAO role.
| source | setup | rights |
|---|---|---|
AWS KMS ECC_NIST_P384, SIGN_VERIFY |
create once, outside the chart | notary: kms:Sign, kms:GetPublicKey on that key |
OpenBAO transit ecdsa-p384 |
bao write transit/keys/audit-seal type=ecdsa-p384 |
notary: update on transit/sign/<key>, read on transit/keys/<key> |
| key file, P-384 PEM (PKCS#8 or SEC 1) | from a Secret | development; the private half sits beside the archive's credentials |
aws kms create-key --key-spec ECC_NIST_P384 --key-usage SIGN_VERIFY \
--description "audit seals"
aws kms create-alias --alias-name alias/sluis-<instance>-audit-seal --target-key-id <key id>
The notary signs a SHA-384 digest. A key of another spec is refused at start. CloudTrail Sign events record when the notary signed.
Give the seal key the policy SealKeyPolicy writes. Only the notary's role signs. The control plane (the key policy, grants, aliases, disabling and deletion) belongs to named key administrators and is denied to every other principal, the account root included.
audit key public --kms-key alias/sluis-<instance>-audit-seal --thumbprint prints the RFC 7638 thumbprint a verifier pins. --jwks prints the JWK Set for keys/roots.jwks. The default is PEM.
The notary writes keys/roots.jwks once if the bucket has none. It refuses to sign if the file omits its key.
Trust is the pin¶
An auditor trusts a key because they pinned its thumbprint, taken from you out of band. Anything under keys/ is distribution. A verifier ignores an unpinned key, so bucket write access cannot add a root. Tell verifiers before the thumbprint changes.
Delegation¶
A root can sign a statement that another key may sign seals for given profiles and tenants for at most 25 hours. audit verify checks the window, scope, limit and revocation, and that a pinned root signed it. The notary does not take a delegation yet: its key must be a root in keys/roots.jwks.
Replacement¶
Replace a signing key freely. A seal names its key by thumbprint, so old seals verify while the verifier pins the old thumbprint too. Keep every public half that ever signed, and every verifier pin. The chain survives: prev hashes the previous seal's bytes.
Platform identities¶
On Kubernetes each component has its own ServiceAccount, bindable through serviceAccount, receiver.serviceAccount, query.serviceAccount and jobs.*.serviceAccount. In stream mode the chart refuses a receiver and writer that share a ServiceAccount name.