@@ -195,7 +195,9 @@ secrets:
195195 objectStorage :
196196 existingSecret : osmo-object-storage
197197 masterEncryptionKey :
198- existingSecret : osmo-master-encryption-key
198+ existingSecret :
199+ name : osmo-master-encryption-key
200+ key : mek.yaml
199201` ` `
200202
201203Keep ` embeddedDependencies.postgresql.enabled: false` (the default), then
@@ -462,14 +464,96 @@ postgresql:
462464 name: osmo-postgresql-credentials
463465` ` `
464466
465- When an external PostgreSQL or Valkey service uses a private CA, enable TLS in
466- the matching `externalDependencies` block and reference the CA Secret there.
467- The Valkey `caKey` must hold a complete PEM trust bundle, including the public
468- or system roots used by other HTTPS endpoints; OSMO's Python services consume
469- that bundle through `SSL_CERT_FILE`. The default Valkey key is `ca-bundle.crt`.
467+ The MEK is mounted through the typed
468+ ` secrets.masterEncryptionKey.existingSecret.{name,key}` reference. Use
469+ `managementMode : external` for an operator-owned read-only Secret. Use
470+ `managementMode : osmo` when this release should create and update that exact
471+ Secret through explicitly requested lifecycle Jobs.
472+
473+ For a disposable install backed by a new database, enable `bootstrap`. Helm
474+ renders no MEK Secret data. A namespace-scoped create-only lifecycle Job waits
475+ for PostgreSQL, proves that the database has no users, UEKs, or dynamic
476+ configuration, verifies that every chart consumer is blocked before its writer
477+ container starts, and atomically creates the full Secret. Key material never
478+ enters Helm output or release state. A retry accepts only the exact Secret owned
479+ by this installation and authenticates the retained database before succeeding;
480+ it never overwrites or deletes a Secret. Non-chart database writers must be
481+ stopped for initial bootstrap.
482+
483+ After the bootstrap Job succeeds, commit and sync
484+ `secrets.masterEncryptionKey.bootstrap.enabled : false`. This mandatory second
485+ Helm/GitOps transaction removes bootstrap Secret-creation RBAC from desired
486+ state. The chart rejects a rotation phase while bootstrap remains enabled.
487+ If bootstrap fails or its database credentials are corrected under Argo CD or
488+ Flux, increment the non-secret `bootstrap.attempt` before syncing again; this
489+ creates a new immutable retry Job without deriving public names from credential
490+ bytes.
491+
492+ Every consumer loads its keyring once at process startup. Before becoming
493+ ready, it performs a bounded authenticated inventory of every UEK wrapper and
494+ registered direct-MEK configuration value. It then logs one machine-readable
495+ ` OSMO_MEK_DESCRIPTOR` containing only the current key ID, loaded key IDs,
496+ generation, and non-secret bundle digest. There are no MEK database tables,
497+ triggers, polling loops, or hot reloads.
498+
499+ Rotation is an explicit three-phase operation. Use one unique request ID for
500+ the whole rotation and keep every previous key in the Secret :
501+
502+ 1. Set `rotation.phase=prepare`. The managed Job adds exactly one key and leaves
503+ ` currentMek` unchanged. After the Job succeeds, clear the phase, change
504+ ` rotation.rolloutRevision` , and sync again to roll every consumer.
505+ 2. Set `rotation.phase=activate`. The Job first verifies that the complete,
506+ Ready Pod cohort belongs to the expected Deployments and logged the PREPARE
507+ descriptor, then selects the new key. Clear the phase, change
508+ ` rolloutRevision` again, and sync to perform the second rollout.
509+ 3. Set `rotation.phase=rewrap`. The Job verifies the ACTIVATE cohort, then
510+ compare-and-swap rewraps all UEKs and registered direct-MEK configuration
511+ from the beginning and runs two authenticated inventories. Clear the phase
512+ after success.
513+
514+ For example, the same values changes work with Helm upgrades or separate Argo
515+ CD syncs :
470516
471- For an external Valkey endpoint signed by a public CA, leave
472- ` caExistingSecret` empty to use the image's system trust store.
517+ ` ` ` yaml
518+ secrets:
519+ masterEncryptionKey:
520+ managementMode: osmo
521+ bootstrap:
522+ enabled: false
523+ attempt: "1" # increment only to retry a failed bootstrap
524+ rotation:
525+ requestId: rotate-2026-08-21
526+ phase: prepare # then "", activate, "", rewrap, ""
527+ rolloutRevision: "1" # change to "2" after PREPARE and "3" after ACTIVATE
528+ ` ` `
529+
530+ Each Job creates or reuses a release-scoped Kubernetes Lease directly. The
531+ Lease is intentionally absent from Helm desired state, so GitOps self-heal
532+ cannot clear a live holder. A Lease held by another attempt is never stolen,
533+ even after its timestamp expires. If an attempt dies, delete its old Job/Pod,
534+ verify it is gone, clear the Lease holder, increment `rotation.attempt`, and
535+ retry the same phase. Jobs never delete Pods or patch Deployments; Helm or the
536+ GitOps controller owns both rollouts. Clear a completed phase promptly so its
537+ narrowly scoped ServiceAccount and RoleBinding leave the desired state.
538+
539+ In `managementMode=external`, the operator performs PREPARE and ACTIVATE by
540+ updating the existing Secret, with one rollout after each update. Then set only
541+ ` rotation.phase=rewrap` . The rewrap Job has exact-name Secret `get` permission,
542+ not `patch` or `update`, and enforces the same ACTIVATE Pod attestation before
543+ touching ciphertext.
544+
545+ Rewrap completion is point-in-time evidence, not permission to remove an old
546+ key. Because this design deliberately has no database write fence, all old MEKs
547+ remain mandatory. User plaintext and UEK key material do not change; only their
548+ encrypted wrappers change. Normal application reads never perform MEK rewrap
549+ writes; the explicit Job is the sole orchestrator.
550+
551+ For an external Valkey endpoint signed by a public CA, enable
552+ ` externalDependencies.valkey.tls.enabled` and leave `caExistingSecret` empty to
553+ use the image's system trust store. For a private CA, set `caExistingSecret` and
554+ ` caKey` in the same block. The selected key must contain the complete trust
555+ bundle because clients use it through `SSL_CERT_FILE`. PostgreSQL private CAs
556+ are also configured in its `externalDependencies` TLS block.
473557
474558# # Exposure
475559
0 commit comments