Skip to content

Commit 04d2b78

Browse files
authored
Support Kubernetes Secret MEK rotation (#1307)
* Support Kubernetes Secret MEK rotation Read MEKs from operator-owned Kubernetes Secrets, with an opt-in create-only bootstrap for disposable installs. Coordinate PREPARE and ACTIVATE through Kubernetes Leases and exact pod startup descriptors, then rewrap UEKs and direct-MEK configuration only through the explicit lifecycle Job. Keep historical MEKs loaded and reject removal. Persist no MEK lifecycle tables, triggers, functions, or key material in PostgreSQL. * Fix MEK CI validation * Fix KIND quick-start MEK bootstrap race * Improve MEK startup diagnostics * Handle empty MEK-encrypted config secrets * Fix MEK KIND Helm lifecycle tests
1 parent 4001736 commit 04d2b78

61 files changed

Lines changed: 5522 additions & 742 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/oetf-kind.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,13 @@ jobs:
189189
df -h /
190190
echo "::endgroup::"
191191
' EXIT
192+
# Keep this below the job's 60-minute ceiling. Without an inner
193+
# deadline, a stuck Helm/Kubernetes wait consumes the whole job and
194+
# GitHub kills it before the diagnostic and artifact steps can run.
195+
# `timeout` returns 124, which is an ordinary step failure, leaving
196+
# roughly ten minutes for cluster state and logs to be collected.
192197
/usr/bin/time -p -o /tmp/oetf.time \
198+
timeout --signal=TERM --kill-after=30s 40m \
193199
bazel run //test/oetf:deploy_and_run -- \
194200
--env kind --tags kind \
195201
--target-pattern //test/smoke/... \
@@ -242,6 +248,13 @@ jobs:
242248
done
243249
echo "::endgroup::"
244250
251+
echo "::group::All OSMO pod logs (last 80 lines)"
252+
for pod in $(kubectl get pods -n osmo -o name 2>/dev/null || true); do
253+
echo "--- osmo/$pod ---"
254+
kubectl logs -n osmo "$pod" --all-containers --tail=80 2>/dev/null || true
255+
done
256+
echo "::endgroup::"
257+
245258
echo "::group::Helm releases (helm list -A)"
246259
helm list -A || true
247260
echo "::endgroup::"

deployments/README.md

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,6 @@ kubectl create secret generic local-admin-password \
161161
--from-literal=password="$LOCAL_ADMIN_PASSWORD" \
162162
--dry-run=client -o yaml | kubectl apply -f -
163163

164-
if ! kubectl get configmap mek-config --namespace osmo >/dev/null 2>&1; then
165-
MEK_KEY=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d '\n')
166-
MEK_JWK=$(printf '{"k":"%s","kid":"key1","kty":"oct"}' "$MEK_KEY" | base64 | tr -d '\n')
167-
MEK_FILE=$(mktemp)
168-
printf 'currentMek: key1\nmeks:\n key1: %s\n' "$MEK_JWK" > "$MEK_FILE"
169-
kubectl create configmap mek-config \
170-
--namespace osmo \
171-
--from-file=mek.yaml="$MEK_FILE" \
172-
--dry-run=client -o yaml | kubectl apply -f -
173-
rm -f "$MEK_FILE"
174-
fi
175-
176164
helm repo add osmo https://helm.ngc.nvidia.com/nvidia/osmo
177165
helm repo update osmo
178166

@@ -187,10 +175,10 @@ helm upgrade --install osmo-backend-operator osmo/backend-operator \
187175
--wait
188176
```
189177

190-
For the local quick-start only, the service values generate
191-
`backend-operator-token` during the first Helm install. Because the backend
192-
operator is installed in the same namespace, it consumes that Secret directly;
193-
no pre-created backend Secret is required.
178+
For the local quick-start only, the service values generate `osmo-mek` and
179+
`backend-operator-token` during the first Helm install. Both are generated
180+
inside Kubernetes and preserved across upgrades; no pre-created MEK or backend
181+
Secret is required. Production values keep both bootstrap modes disabled.
194182

195183
After installing the CLI and logging in, set the demo pool and LocalStack data credential:
196184

deployments/charts/osmo/README.md

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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
201203
Keep `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

deployments/charts/osmo/profiles/kind-self-contained.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ secrets:
4444
managedSecret:
4545
name: osmo-backend-token
4646
masterEncryptionKey:
47-
generate: true
48-
existingSecret: ''
47+
managementMode: osmo
48+
existingSecret:
49+
name: osmo-master-encryption-key
50+
key: mek.yaml
51+
bootstrap:
52+
enabled: true
4953

5054
services:
5155
mcp:

deployments/charts/osmo/profiles/split-plane-control.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,6 @@ secrets:
115115
objectStorage:
116116
existingSecret: osmo-object-storage
117117
masterEncryptionKey:
118-
existingSecret: osmo-master-encryption-key
118+
existingSecret:
119+
name: osmo-master-encryption-key
120+
key: mek.yaml

deployments/charts/osmo/templates/_helpers.tpl

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -393,26 +393,23 @@ data:
393393
{{- end -}}
394394

395395
{{- define "osmo.secrets.mekVolume" -}}
396-
{{- with (include "osmo.masterEncryptionKey.secretName" .) }}
396+
{{- with .Values.secrets.masterEncryptionKey.existingSecret.name }}
397397
- name: mek-volume
398398
secret:
399-
secretName: {{ . }}
399+
secretName: {{ . | quote }}
400400
items:
401-
- key: {{ $.Values.secrets.masterEncryptionKey.keys.config }}
402-
path: mek.yaml
401+
- key: {{ required "secrets.masterEncryptionKey.existingSecret.key is required" $.Values.secrets.masterEncryptionKey.existingSecret.key | quote }}
402+
path: "mek.yaml"
403403
{{- end }}
404404
{{- end -}}
405405

406-
{{- define "osmo.masterEncryptionKey.secretName" -}}
407-
{{- if .Values.secrets.masterEncryptionKey.generate -}}
408-
{{- printf "%s-master-encryption-key" (include "osmo.fullname" .) | trunc 63 | trimSuffix "-" -}}
409-
{{- else -}}
410-
{{- .Values.secrets.masterEncryptionKey.existingSecret -}}
411-
{{- end -}}
412-
{{- end -}}
406+
{{- define "osmo.secrets.mekFile" -}}/opt/osmo/mek/mek.yaml{{- end -}}
407+
{{- define "osmo.secrets.mekMountPath" -}}/opt/osmo/mek{{- end -}}
413408

414-
{{- define "osmo.masterEncryptionKey.bootstrapName" -}}
415-
{{- printf "%s-mek-bootstrap" (include "osmo.fullname" .) | trunc 63 | trimSuffix "-" -}}
409+
{{- define "osmo.secrets.mekRolloutAnnotation" -}}
410+
{{- with .Values.secrets.masterEncryptionKey.rotation.rolloutRevision }}
411+
osmo.nvidia.com/mek-rollout: {{ . | quote }}
412+
{{- end }}
416413
{{- end -}}
417414

418415
{{- define "osmo.valkey.fullname" -}}

deployments/charts/osmo/templates/agent-service.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ spec:
4646
{{- include "osmo.pod.labels" (dict "root" . "component" .Values.services.agent "componentName" "agent") | nindent 8 }}
4747
annotations:
4848
{{- include "osmo.pod.annotations" (dict "root" . "component" .Values.services.agent) | nindent 8 }}
49+
{{- include "osmo.secrets.mekRolloutAnnotation" . | nindent 8 }}
4950
spec:
5051
automountServiceAccountToken: {{ .Values.services.agent.serviceAccount.automountServiceAccountToken }}
5152
securityContext:
@@ -107,9 +108,9 @@ spec:
107108
- {{ include "osmo.postgresql.port" . | quote }}
108109
- --postgres_database_name
109110
- {{ include "osmo.postgresql.database" . }}
110-
{{- if (include "osmo.masterEncryptionKey.secretName" .) }}
111+
{{- if .Values.secrets.masterEncryptionKey.existingSecret.name }}
111112
- --mek_file
112-
- {{ .Values.secrets.masterEncryptionKey.mountPath }}
113+
- {{ include "osmo.secrets.mekFile" . | quote }}
113114
{{- end}}
114115
{{- with (include "osmo.postgresql.username" .) }}
115116
- --postgres_user
@@ -138,10 +139,10 @@ spec:
138139
mountPath: /tmp
139140
- name: osmo-progress-files
140141
mountPath: /var/run/osmo
141-
{{- if (include "osmo.masterEncryptionKey.secretName" .) }}
142-
- mountPath: {{ .Values.secrets.masterEncryptionKey.mountPath }}
142+
{{- if .Values.secrets.masterEncryptionKey.existingSecret.name }}
143+
- mountPath: {{ include "osmo.secrets.mekMountPath" . | quote }}
143144
name: mek-volume
144-
subPath: mek.yaml
145+
readOnly: true
145146
{{- end }}
146147
{{- include "osmo.configuration.volumeMounts" . | nindent 8 }}
147148
{{- include "osmo.externalDependencies.caVolumeMounts" . | nindent 8 }}

deployments/charts/osmo/templates/api-service.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ spec:
4747
{{- include "osmo.pod.labels" (dict "root" . "component" .Values.services.api "componentName" "api") | nindent 8 }}
4848
annotations:
4949
{{- include "osmo.pod.annotations" (dict "root" . "component" .Values.services.api "protectedAnnotations" $protectedPodAnnotations) | nindent 8 }}
50+
{{- include "osmo.secrets.mekRolloutAnnotation" . | nindent 8 }}
5051
spec:
5152
automountServiceAccountToken: {{ .Values.services.api.serviceAccount.automountServiceAccountToken }}
5253
securityContext:
@@ -102,9 +103,9 @@ spec:
102103
- {{ include "osmo.postgresql.port" . | quote }}
103104
- --postgres_database_name
104105
- {{ include "osmo.postgresql.database" . }}
105-
{{- if (include "osmo.masterEncryptionKey.secretName" .) }}
106+
{{- if .Values.secrets.masterEncryptionKey.existingSecret.name }}
106107
- --mek_file
107-
- {{ .Values.secrets.masterEncryptionKey.mountPath }}
108+
- {{ include "osmo.secrets.mekFile" . | quote }}
108109
{{- end }}
109110
{{- with (include "osmo.postgresql.username" .) }}
110111
- --postgres_user
@@ -180,10 +181,10 @@ spec:
180181
volumeMounts:
181182
- name: osmo-runtime-tmp
182183
mountPath: /tmp
183-
{{- if (include "osmo.masterEncryptionKey.secretName" .) }}
184-
- mountPath: {{ .Values.secrets.masterEncryptionKey.mountPath }}
184+
{{- if .Values.secrets.masterEncryptionKey.existingSecret.name }}
185+
- mountPath: {{ include "osmo.secrets.mekMountPath" . | quote }}
185186
name: mek-volume
186-
subPath: mek.yaml
187+
readOnly: true
187188
{{- end }}
188189
{{- if .Values.secrets.backendApiTokens.enabled }}
189190
{{- range .Values.secrets.backendApiTokens.credentials }}

deployments/charts/osmo/templates/delayed-job-monitor.yaml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ spec:
4040
labels:
4141
{{- include "osmo.pod.labels" (dict "root" . "component" .Values.services.delayedJobMonitor "componentName" "delayed-job-monitor") | nindent 8 }}
4242
annotations:
43-
{{- include "osmo.pod.annotations" (dict "root" . "component" .Values.services.delayedJobMonitor) | nindent 8 }}
43+
{{- include "osmo.pod.annotations" (dict "root" . "component" .Values.services.delayedJobMonitor) | nindent 8 }}
44+
{{- include "osmo.secrets.mekRolloutAnnotation" . | nindent 8 }}
4445
spec:
4546
automountServiceAccountToken: {{ .Values.services.delayedJobMonitor.serviceAccount.automountServiceAccountToken }}
4647
securityContext:
@@ -98,9 +99,9 @@ spec:
9899
- {{ include "osmo.postgresql.port" . | quote }}
99100
- --postgres_database_name
100101
- {{ include "osmo.postgresql.database" . }}
101-
{{- if (include "osmo.masterEncryptionKey.secretName" .) }}
102+
{{- if .Values.secrets.masterEncryptionKey.existingSecret.name }}
102103
- --mek_file
103-
- {{ .Values.secrets.masterEncryptionKey.mountPath }}
104+
- {{ include "osmo.secrets.mekFile" . | quote }}
104105
{{- end}}
105106
{{- with (include "osmo.postgresql.username" .) }}
106107
- --postgres_user
@@ -127,10 +128,10 @@ spec:
127128
volumeMounts:
128129
- name: osmo-progress-files
129130
mountPath: /var/run/osmo
130-
{{- if (include "osmo.masterEncryptionKey.secretName" .) }}
131-
- mountPath: {{ .Values.secrets.masterEncryptionKey.mountPath }}
131+
{{- if .Values.secrets.masterEncryptionKey.existingSecret.name }}
132+
- mountPath: {{ include "osmo.secrets.mekMountPath" . | quote }}
132133
name: mek-volume
133-
subPath: mek.yaml
134+
readOnly: true
134135
{{- end }}
135136
{{- include "osmo.component.extraVolumeMounts" .Values.services.delayedJobMonitor | nindent 8 }}
136137
{{- include "osmo.externalDependencies.caVolumeMounts" . | nindent 8 }}

deployments/charts/osmo/templates/logger-service.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ spec:
4646
{{- include "osmo.pod.labels" (dict "root" . "component" .Values.services.logger "componentName" "logger") | nindent 8 }}
4747
annotations:
4848
{{- include "osmo.pod.annotations" (dict "root" . "component" .Values.services.logger) | nindent 8 }}
49+
{{- include "osmo.secrets.mekRolloutAnnotation" . | nindent 8 }}
4950
spec:
5051
automountServiceAccountToken: {{ .Values.services.logger.serviceAccount.automountServiceAccountToken }}
5152
securityContext:
@@ -102,9 +103,9 @@ spec:
102103
- {{ include "osmo.postgresql.port" . | quote }}
103104
- --postgres_database_name
104105
- {{ include "osmo.postgresql.database" . }}
105-
{{- if (include "osmo.masterEncryptionKey.secretName" .) }}
106+
{{- if .Values.secrets.masterEncryptionKey.existingSecret.name }}
106107
- --mek_file
107-
- {{ .Values.secrets.masterEncryptionKey.mountPath }}
108+
- {{ include "osmo.secrets.mekFile" . | quote }}
108109
{{- end}}
109110
{{- with (include "osmo.postgresql.username" .) }}
110111
- --postgres_user
@@ -133,10 +134,10 @@ spec:
133134
mountPath: /tmp
134135
- name: osmo-progress-files
135136
mountPath: /var/run/osmo
136-
{{- if (include "osmo.masterEncryptionKey.secretName" .) }}
137-
- mountPath: {{ .Values.secrets.masterEncryptionKey.mountPath }}
137+
{{- if .Values.secrets.masterEncryptionKey.existingSecret.name }}
138+
- mountPath: {{ include "osmo.secrets.mekMountPath" . | quote }}
138139
name: mek-volume
139-
subPath: mek.yaml
140+
readOnly: true
140141
{{- end }}
141142
{{- include "osmo.configuration.volumeMounts" . | nindent 8 }}
142143
{{- include "osmo.externalDependencies.caVolumeMounts" . | nindent 8 }}

0 commit comments

Comments
 (0)