ADR-007: KEK Lifecycle and Rotation
Status: Accepted Date: 2026-05-27 Sprint: SPRINT_20260529_028_Key_management_operations.md (RP-028-001…008, RP-028-014) Related ADRs: ADR-004 (forward-only migrations), ADR-005 (single-operator multi-environment tenancy), ADR-006 (execution plugin framework — same plugin-host substrate the HSM source bridges through) Related runbooks: KEK rotation, KEK source: Vault, KEK source: HSM, credential-storage algorithm switch
This record makes the deployment-secret master KEK rotatable, observable, and source-pluggable (env, file, Vault, HSM) without downtime or data loss, and audits every rotation. Read it before touching any credential-ciphertext store, the KEK source configuration, or the crypto-control surface. Operators rotating a live KEK should follow the KEK rotation runbook.
Implementation status (2026-07-18). The accepted architecture below is still the target, not the complete current runtime contract. The wired Platform launcher passes the same AEAD instance as both the old and new key, then records a new version with the active source and fingerprint. Current operations therefore perform a resumable current-material re-seal/version bump; they do not apply a selected source, source reference, grace window, or changed KEK material. Platform responses expose this truth through
rotationSemantics(operationMode=current_material_reseal,changesKeyMaterial=false, and all source/reference/grace applied flags false). The CLI source/reference/grace options and the console wizard inputs must not be treated as applied until a coordinated Platform + Integrations + ReleaseOrchestrator successor supplies both old and new key resolvers, a dual-key grace lifecycle, and forcing-function proof of changed fingerprints, old/new ciphertext openability, and retirement after grace. See the rotation runbook for the operational limit.
Context
Sprint 027 shipped the dual-mode deployment-secret crypto stack. Three ciphertext stores share a single master KEK (“key-encryption key”) protecting their per-row data-encryption keys:
- Platform —
platform.connector_credentials(operator-entered SCM / registry / Vault / chat credentials) - Integrations —
integrations.integrationsinline credentials (the in-process equivalent for connector plugins) - ReleaseOrchestrator —
release_orchestrator.deployment_bundles(push-mode bundles containing per-deployment secrets)
After Sprint 027 the master KEK was resolved from a single env var (STELLAOPS_BOOTSTRAP_KEY and per-store fallbacks) at process start. This left four gaps:
- No rotation surface. Operators had no way to roll the KEK without stopping every service, editing env vars, and accepting that every existing ciphertext became unreadable. There was no “grace window” where both the old and new KEK were resident in process memory, so any rotation that was not preceded by a manual re-seal of every row resulted in data loss.
- No visibility into per-blob KEK version distribution. Operators could not tell which rows were sealed under which KEK version. After a rotation, there was no signal that a row still pinned to the old KEK had been missed.
- No path for KMS / HSM unwrap. The env-var-only design closed the door on operators in regulated environments who require their KEK to live in HashiCorp Vault, a PKCS#11 HSM, or a TPM. The “secret bytes in an env var” shape is the inverse of what those operators must defend.
- No audit trail. Rotation events (when, by whom, from which source, to which source, succeeded or aborted) were invisible to compliance audit.
Sprint 027’s Decisions & Risks called this out explicitly as the lone unfinished thread blocking production-grade adoption of the dual-mode crypto stack in regulated environments.
Decision
Introduce a pluggable IKekSourceabstraction that materialises the master KEK at process start and provides a re-resolve hook for rotation. Track KEK versions in a new crypto.kek_versionsledger table — one row per (kek_id, version) — that records the source, state, non-secret fingerprint, activator, and timestamp for every version that has ever been active. Drive multi-store re-seal through a shared CredentialReSealWalkerlibrary that re-seals every row across the three stores idempotently and resumably. Expose the operator-control surface through Platform admin endpoints, the stella crypto kek CLI subgroup, and the console-admin /console-admin/crypto-control UI. Every rotation event is audited. KEK material never enters audit, logs, UI payloads, HTTP responses, or any exception that escapes the seal/open call boundary.
IKekSource shapes
record KekMaterial(string KekId, int KekVersion,
ReadOnlyMemory<byte> SecretBytes,
DateTimeOffset? ExpiresAt);
interface IKekSource
{
Task<KekMaterial> ResolveAsync(KekSourceContext ctx, CancellationToken ct);
}
Production-ready sources in this sprint:
| Source | Library | Purpose |
|---|---|---|
env | StellaOps.Cryptography.CredentialStore (EnvKekSource) | Default; byte-identical to Sprint 027 resolution. |
file | StellaOps.Cryptography.CredentialStore (FileKekSource) | Cheap hardening; owner-only ACL enforced. |
vault | StellaOps.Cryptography.CredentialStore.Vault (flagship) | Reuses operator’s existing on-prem HashiCorp Vault. |
hsm | StellaOps.Cryptography.CredentialStore.Hsm | PKCS#11 unwrap via StellaOps.Cryptography.Plugin.Hsm. |
Source selection is driven by the Crypto:Kek:Source config key. Unknown values fail at registration (no silent downgrade). Default = env, preserving Sprint 027 behaviour.
crypto.kek_versions ledger
CREATE TABLE IF NOT EXISTS crypto.kek_versions (
kek_id VARCHAR(80) NOT NULL,
version INT NOT NULL,
state VARCHAR(20) NOT NULL, -- active | grace | retired | rotating
source VARCHAR(40) NOT NULL, -- env | file | vault | hsm
source_metadata JSONB NULL, -- e.g. {"vault_path":"secret/stellaops/kek/v2"}
activated_at TIMESTAMPTZ NOT NULL,
retired_at TIMESTAMPTZ NULL,
activated_by VARCHAR(160) NOT NULL,
fingerprint VARCHAR(64) NOT NULL, -- non-secret; 16-hex truncation of double-SHA-256
PRIMARY KEY (kek_id, version)
);
The fingerprint is a one-way 64-bit truncation of a double-SHA-256 with a KDF label binding (KekFingerprint.Compute). It is non-secret and used to let operators eyeball “is this the KEK I think it is?” without ever rendering KEK material. The KEK bytes themselves never enter the ledger.
The shared library StellaOps.Cryptography.CredentialStore.Persistence ships the schema as an embedded resource so all three services migrate identically. Migration is forward-only per ADR-004.
Per-row sealed-by-KEK-version + algorithm tracking
Every ciphertext row carries (kek_id, kek_version, algorithm). Sprint 027 rows defaulted to (kek_id=<resolver-derived>, kek_version=1, algorithm=AES-256-GCM); the additive migrations (Platform 080, Integrations 004, ReleaseOrchestrator 013) backfill the defaults on existing rows. This gives the walker a deterministic predicate (secret_kek_version < <active>) for “rows still pinned to an older KEK” and makes per-version blob population queryable without scanning ciphertext.
Multi-store re-seal walker
CredentialReSealWalker operates over the abstract ICredentialReSealStore port (CountByKekVersionAsync + ReSealBatchAsync). Each of the three services implements the port over its own ciphertext store:
ConnectorCredentialReSealStore(Platform)IntegrationCredentialReSealStore(Integrations)DeploymentBundleReSealStore(ReleaseOrchestrator)
All three use SELECT ... FOR UPDATE SKIP LOCKED for safe concurrency, scrub per-row plaintext in finally, and perform an atomic swap of (nonce, ciphertext, kek_version, alg, kek_id). Progress is persisted in crypto.reseal_progress so a crashed walker resumes automatically via CredentialReSealResumeHostedService. Batch=256 (configurable), per-batch timeout 30s, rate-limited (default 100 batches/sec) so an O(million)-row rotation can be wall-clock-windowed.
Control plane
- Admin endpoints on Platform (
/api/v1/admin/crypto/kek/*):status,history,rotation-plan(dry-run),rotate,rotation/{id}/progress,rotation/{id}/abort,blob-populations. Cross-service aggregation goes through typed-HTTPICrossServiceCryptoControlClientinstances with mTLS pluggable at the named-HttpClient site. Per-downstream failure is isolated (one unreachable service does not break the control plane). - Scopes:
crypto:kek:read,crypto:kek:rotate.crypto:kek:rotateimpliescrypto:kek:read;ops.adminsatisfies both. - CLI:
stella crypto kek {status,history,rotate,abort}+stella crypto profile validate. Exit codes 0 / 1 / 2 distinguish completion / partial-or-aborted / hard-fail. - UI:
/console-admin/crypto-controlwith three panels (Master Keys, Blob Populations, Rotation History) and a 4-step rotate wizard (pick-source → reference → fingerprint preview → confirm). The wizard’s template carries no field named*key*/*kek*/*secret*/*material*(regex-asserted); operators type only references (Vault path, file path, env var name) — never bytes. - Audit:
RotateKekDryRun,RotateKekBegin,RotateKekStep,RotateKekComplete,RotateKekAbort. Payloads carry only operator-visible identifiers and sanitised error messages.
Consequences
Positive
- Operators rotate KEKs without downtime through a single CLI invocation (
stella crypto kek rotate --confirm) or through the console-admin wizard. Both surfaces have a dry-run first; the dry-run is mutex with--confirm. - Full visibility per kek_id, per version, per store. The Blob Populations panel and the
blob-populationsendpoint surface the distribution end-to-end. RED-alert colouring fires when any row remains pinned to a retired KEK version (which should never happen if the grace window was respected). - Clear abort path.
stella crypto kek abortand the UI abort button transition the rotation toabortedand the walker stops respecting the rotation_id. The retired KEK remains loaded in process memory through the grace window so partially-rotated rows continue to open under the old key until they are re-sealed. - Vault flagship reuses the operator’s existing on-prem Vault from Sprint 027’s pull-mode AuthRef configuration. No new infrastructure for operators who already run Vault.
- HSM via the existing
StellaOps.Cryptography.Plugin.Hsmcovers hardware-validated installs. The narrowIHsmKekUnwrapperport keeps the CredentialStore library plugin-agnostic so operators with bespoke HSM stacks can implement the port directly. - Per-blob
sealed-by-KEK-versiontracking enables compliance reporting: “how many rows are sealed under the post-incident KEK?” is a one-query answer.
Negative
- Schema churn (additive per ADR-004): one new column on each ciphertext table (
secret_kek_version INT NOT NULL DEFAULT 1), one newcrypto.kek_versionstable, one newcrypto.reseal_progresstable. All migrations are idempotent and forward-only. - Operational complexity: rotation is a multi-store operation that must either complete or be aborted explicitly. The grace window during which two KEK versions are resident in process memory expands the in-process secret attack surface for the duration of the window (default 24h, operator-tunable).
- Cross-service coordination: Platform aggregates progress from Integrations and ReleaseOrchestrator via internal mTLS endpoints. The control plane degrades gracefully if one downstream is unreachable, but visibility into that downstream’s progress is suspended until it returns.
Neutral
- Env-var deployments are byte-identical to Sprint 027 behaviour: the default
IKekSourceregistration isEnvKekSource, which preserves the same fallback chain (STELLAOPS_BOOTSTRAP_KEY→ per-store fallbacks) byte-for-byte. Operators who do not engage with the rotation surface see no change. - Architecture-conformance tests (
AeadBclCallSiteConformanceTests,RandomBclCallSiteConformanceTests) continue to prove that no direct BCL AEAD / random call sites exist outside the canonical crypto assembly + the sanctioned allow-list (the default credential primitive’sChaCha20Poly1305enum-token reference and the in-memory unwrap fixture inHsmKekSourceTests).
Alternatives considered (rejected)
Keep env-var-only; operators rotate via stop / swap-env / restart + custom one-time scripts
Rejected. Not auditable, not safe at scale, and blocks regulated-environment adoption. The “I edited the env var on three hosts and restarted” workflow provides no record of who, when, or which version, and offers no protection against forgetting one of the three ciphertext stores. Every Sprint 027 operator who asked about rotation hit this wall.
Rotation via one-shot Kubernetes Job
Rejected. Ties the control plane to a deployment model the operator may not use — Stella Ops targets non-Kubernetes estates. A CLI-driven control plane is portable across docker-compose, Nomad, bare-metal systemd, and the operator’s hosted Kubernetes (which can call the CLI from a Job if they so choose). The reverse — making the control plane K8s-native — is not portable.
AWS KMS / Azure Key Vault as production sources or DI-registered stubs
Rejected. Stella Ops targets self-hosted, offline/air-gap-capable on-premises estates. Cloud-managed KMS contradicts that posture and was the explicit operator decision recorded in the Sprint 028 dossier (2026-05-27). The IKekSource interface remains open for downstream installs that need it — they implement the plugin in their own assembly. Shipping a stub that throws NotImplementedException would imply support not delivered and appear in install scans as a “supported but unconfigured” source, which is the inverse of the truth.
Auto-scheduled rotation
Deferred. Rotation is a high-trust operation; the operator should drive cadence in this generation of the surface. The schema, walker, and audit trail are all rotation-id-driven, so scheduling can be layered on top later without re-doing the foundation.
KEK material in crypto.kek_versions (encrypted under an HSM-only KEK)
Rejected. Even encrypted-at-rest, this would put KEK material on the admin DB’s backup tape, in PITR archives, and in operator psql sessions. The ledger holds only metadata + a non-secret fingerprint; the KEK material lives exclusively in the configured IKekSource and in process memory.
Per-tenant KEK isolation
Out of scope. ADR-005 establishes that per-tenant isolation is achieved cryptographically via the per-tenant HKDF DEK derivation. The master KEK is per-installation. Per-tenant KEK is desirable but is its own sprint (key custody, ceremony tooling, tenant-lifecycle integration).
References
- Sprint 028 dossier:
docs/implplan/SPRINT_20260529_028_Key_management_operations.md(ordocs-archive/implplan/...after closeout). - Commits:
801118a3ccRP-028-001IKekSource+EnvKekSource+FileKekSourcece2ffa76ecRP-028-002crypto.kek_versionstable + repository4be41f8104RP-028-003 per-row(kek_id, kek_version, algorithm)e281d9d1e5RP-028-004CredentialReSealWalker+ per-store adaptersaa0e8aa6eeRP-028-005 Platform admin endpoints + cross-service mTLS + scopesc8203827f1RP-028-006stella crypto kekCLI subgroupb7092a5597RP-028-007 console-admin crypto-control panels71f7c67f3fRP-028-008 Vault flagship + HSM bridge
- ADR-004: Forward-Only Database Migrations
- ADR-005: Single-Operator Multi-Environment Tenancy
- ADR-006: Execution Plugin Framework
- ADR-008: Credential Storage Regional Algorithm Routing
