Skip to content

Security Model

Threat model, security invariants, and vulnerability disclosure.

Documented configuration defaults (transport and identity are separate):

tls: null
authn:
type: mtls
  1. Client ↔ Service — TLS is opt-in: the tls: block enables gRPC TLS, and tls.ca_cert enables client-certificate verification. The REST listener serves plain HTTP; terminate HTTPS at a reverse proxy and protect the proxy-to-service connection. Authentication defaults to mtls, but that choice does not enable transport TLS. JWT bearer authentication and other credential types require explicit configuration; REST cannot use the gRPC peer-certificate identity. The service trusts the PDP for authorization decisions.
  2. Service ↔ PDP — If the PDP is compromised, authorization is compromised. The service fails closed if the PDP is unreachable.
  3. Service ↔ Storage — Storage holds encrypted key handles and metadata. Key material lives in the provider, not storage.
  4. Service ↔ HSM — The HSM is the root of trust. PKCS#11 PIN is zeroized after session establishment.

These invariants are enforced structurally in the codebase:

  1. Authorization on every operation — Every handler passes through PDP authorization before executing.
  2. Audit on every operation — Including denied requests (AuthorizationDenied).
  3. No implicit authorization policy — There is no default PDP. A config that omits the pdp: block is a startup error, so the service cannot reach a serving state without an explicit authorization decision. Disabling authorization (always_allow) is available but must be written out, and is named in a warning at every startup.
  4. Fail-closed on PDP unavailability — No “allow if PDP is down” mode.
  5. Fail-closed on KMS unavailability — No fallback to weaker algorithms.
  6. Sensitive data zeroization — Key material, plaintext, and DEKs use Sensitive<T> wrapper.
  7. Identity tags excluded from responses — Only user_tags returned to API callers.
  8. Opaque encryption context — AAD is BLAKE3-hashed in audit logs; raw values never persisted.
  9. Cascade disable — Disabling a parent key disables all descendants server-side.
  10. Unique LIDs — UUID injection makes LID collisions structurally impossible.
  11. Constant-time authentication — Bootstrap token comparison uses constant-time equality.
PurposeAlgorithm
Symmetric encryptionAES-256-GCM
SigningEd25519, ECDSA P-256, RSA PKCS#1v1.5
Internal hashingBLAKE3
Wire-boundary hashingSHA-256

Tamper-evidence and authenticity are separate properties, configured separately.

Hash chaining is unconditional. Every audit event carries BLAKE3 hash-chain linking in every deployment, whether or not signing is enabled. Each link commits to the preceding event, and the preimage is present verbatim in the written log, so keyrack audit verify <log> re-derives the chain with no key at all and detects modified, deleted, and reordered events.

Two bounds on keyless verification, both of which signing or an external anchor closes:

  1. Full-log rewrite. Keyless verification catches edits made in place. Repairing the chain after an edit means recomputing every link from that point forward, which an attacker with write access to the whole log can do. A chain therefore does not establish who wrote the log. Ed25519 signatures do, because the attacker would also need the signing key.
  2. Tail-truncation. Dropping the newest N events breaks nothing internal to the log, and signing does not help either. Detecting it requires an external anchor such as a signed checkpoint or an independent witness.

Ed25519 signing is opt-in and adds authorship on top of the chain. Enabling it requires a persistent signing key — KeyRack refuses to start with a per-startup key unless that is explicitly opted into, because signatures written before a restart would otherwise be permanently unverifiable.

Events are delivered via NATS for distributed consumption.

Report security issues responsibly via the process documented in the upstream SECURITY.md.

See also: Integration guide · Operator guide