Skip to content

How is storage structured?

product (sluis, audit)
   │  asks by purpose / by path
   ├── storage/keys    Key: Encrypt, Decrypt, GenerateDataKey, MAC, PublicKey, Sign (direct signing is deprecated)
   │      backends:    kms · transit · local (tests)
   └── storage/state   Store, Value[T]: versioned JSON objects
          backends:    ssm · s3 · openbao (KV v2) · memory (tests)

storage/openbao   one client (JWT login, namespace, CA bundle) shared by state/openbao and keys/transit

state is a small versioned key-value store for what a service keeps between runs: signing keys, issued credentials and rotation state.

A Value[T] is a typed handle on one key. Rotating(grace) also returns the value it replaced for a grace period. Every value is a JSON object and every write returns a revision. A write is conditional on the revision the caller last saw, so a lost race is an error and never a silent overwrite.

The store knows nothing about what it holds. Each product adds a typed layer: an internal view for what only the service reads and writes, and an external view for what it publishes or receives.

keys gives a caller one way to ask for a key: by purpose (sign, seal, pseudonym, conceal, archive). The configuration names the key, and a backend does the work. A ciphertext is bound to a context, by default the instance and the purpose. Two deployments or purposes that share a key cannot open each other's ciphertexts.

One OpenBAO client. A process builds one openbao.Client and gives it to both backends. It logs in lazily with a JWT auth mount and a projected ServiceAccount token, and re-reads the token file at every login. It keeps its token until 80% of the lease has gone.

Each backend passes a conformance suite, storage/state/conformance or storage/keys/conformance. A product behaves the same whichever backend it gets. The block that selects them is the adapter block.

Decided in