-
Notifications
You must be signed in to change notification settings - Fork 13
feat(operator-wandb): add session key rotation #661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
39d244d
feat(operator-wandb): add session key rotation
zacharyblasczyk b28accc
feat(operator-wandb): harden session key rotation
zacharyblasczyk c79b704
chore(operator-wandb): bump chart version
zacharyblasczyk c2d665a
fix(operator-wandb): reject empty rotation candidate
zacharyblasczyk 6e686d2
docs(operator-wandb): document secret reload rollouts
zacharyblasczyk 87193e0
refactor(operator-wandb): clarify previous session keys env
zacharyblasczyk 79dbf06
Merge origin/main into agent/operator-session-key-rotation
zacharyblasczyk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| # Session Key Rotation | ||
|
|
||
| `operator-wandb` generates `GORILLA_SESSION_KEY` on first install and stores it | ||
| in `<release>-gorilla-session-key`. Later reconciliations retain the stored key. | ||
|
|
||
| The chart can rotate this managed key without immediately invalidating existing | ||
| sessions. The routine workflow has three phases so every running pod learns the | ||
| candidate key before any pod starts signing with it. | ||
|
|
||
| ## Managed Rotation | ||
|
|
||
| Choose a unique rotation ID and start with `prepare`: | ||
|
|
||
| ```yaml | ||
| global: | ||
| auth: | ||
| sessionKeyRotation: | ||
| id: "2026-07-session-key" | ||
| phase: prepare | ||
| ``` | ||
|
|
||
| `prepare` generates a candidate key, keeps the current signing key, and adds the | ||
| candidate to `GORILLA_SESSION_KEY_PREVIOUS`. The rotation ID and phase also | ||
| change the API and app pod templates, which starts a rollout. Wait for every | ||
| enabled API and app pod to finish rolling before continuing. This wait is the | ||
| critical safety step: during the next rollout, prepared pods sign with A and | ||
| accept B, while activated pods sign with B and accept A. | ||
|
|
||
| Promote the candidate with the same ID: | ||
|
|
||
| ```yaml | ||
| global: | ||
| auth: | ||
| sessionKeyRotation: | ||
| id: "2026-07-session-key" | ||
| phase: activate | ||
| ``` | ||
|
|
||
| `activate` makes the candidate the current signing key and retains the outgoing | ||
| key in `GORILLA_SESSION_KEY_PREVIOUS`. Reapplying the same ID and phase does not | ||
| swap the keys again. | ||
|
|
||
| After the longest-lived session or token has expired, remove the verification | ||
| keys: | ||
|
|
||
| ```yaml | ||
| global: | ||
| auth: | ||
| sessionKeyRotation: | ||
| id: "2026-07-session-key" | ||
| phase: clear | ||
| ``` | ||
|
|
||
| Wait for each rollout to complete before advancing the phase. Use a new ID for | ||
| the next rotation. To cancel before activation, advance a prepared rotation | ||
| directly to `clear`; the current signing key will remain unchanged. | ||
|
|
||
| ## Choose a Rotation Policy | ||
|
|
||
| The phase sequence and wait policy determine the response: | ||
|
|
||
| | Policy | Procedure | User impact | Use when | | ||
| | ------ | --------- | ----------- | -------- | | ||
| | Routine graceful | `prepare`, `activate`, wait out the longest-lived cookie or token, then `clear` | Existing sessions and tokens remain valid | Scheduled rotation with no suspected exposure | | ||
| | Accelerated | `prepare` and wait, `activate` and wait, then `clear` immediately | Avoids cross-generation login loops, but invalidates sessions and tokens signed by the old key | A leak is plausible, but there is no evidence of active forgery | | ||
| | Emergency hard cutover | Apply `hard-cutover` with a new rotation ID | Logs users out and may cause transient authentication failures during rollout | There is high confidence of compromise or active abuse | | ||
|
|
||
| For an accelerated rotation, "immediately" means there is no token-lifetime | ||
| overlap period after the activation rollout. Do not advance the phase while a | ||
| rollout is still in progress. | ||
|
|
||
| An emergency hard cutover generates a new managed current key and removes all | ||
| previous verification keys in one reconciliation: | ||
|
|
||
| ```yaml | ||
| global: | ||
| auth: | ||
| sessionKeyRotation: | ||
| id: "incident-2026-07-29" | ||
| phase: hard-cutover | ||
| ``` | ||
|
|
||
| This deliberately breaks compatibility between old and new pods. Coordinate it | ||
| as a disruptive change and complete the API and app rollouts as quickly as is | ||
| safe. Reapplying the same ID and phase retains the generated key; use a unique | ||
| ID for every hard cutover. A hard cutover with a new ID can interrupt a | ||
| prepared or activated rotation. | ||
|
|
||
| ## External Secrets | ||
|
|
||
| `global.auth.sessionKey` and `global.auth.sessionKeyPrevious` accept literal | ||
| values or Kubernetes `valueFrom` maps. Prefer Secret references so key material | ||
| does not appear in rendered manifests: | ||
|
|
||
| ```yaml | ||
| global: | ||
| auth: | ||
| sessionKey: | ||
| valueFrom: | ||
| secretKeyRef: | ||
| name: gorilla-session-keys | ||
| key: current | ||
| sessionKeyPrevious: | ||
| valueFrom: | ||
| secretKeyRef: | ||
| name: gorilla-session-keys | ||
| key: previous | ||
| sessionKeyRolloutId: "2026-07-session-key-prepare" | ||
| ``` | ||
|
|
||
| For external keys, create and update `gorilla-session-keys` separately. A Secret | ||
| data update does not change environment variables in running containers. After | ||
| each Secret update, change `sessionKeyRolloutId` and apply the Helm release to | ||
| force API and app rollouts: | ||
|
|
||
| 1. **Prepare:** set `current` to A and `previous` to B, set the rollout ID to a | ||
| unique value ending in `-prepare`, apply the release, and wait for rollout. | ||
| 2. **Activate:** set `current` to B and `previous` to A, change the rollout ID | ||
| to a value ending in `-activate`, apply the release, and wait for rollout. | ||
| 3. **Clear:** set `previous` to an empty value, change the rollout ID to a value | ||
| ending in `-clear`, apply the release, and wait for rollout. | ||
|
|
||
| `sessionKeyRolloutId` is only a pod-template marker; it must not contain key | ||
| material. External key overrides cannot be combined with | ||
| `sessionKeyRotation`, but they can and should be combined with | ||
| `sessionKeyRolloutId`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| {{- define "wandb.sessionKeyEnvs" -}} | ||
| {{- with .Values.global.auth.sessionKey }} | ||
| - name: GORILLA_SESSION_KEY | ||
| {{- if kindIs "map" . }} | ||
| {{- toYaml . | nindent 2 }} | ||
| {{- else }} | ||
| value: {{ . | toString | quote }} | ||
| {{- end }} | ||
| {{- end }} | ||
| {{- with .Values.global.auth.sessionKeyPrevious }} | ||
| - name: GORILLA_SESSION_KEY_PREVIOUS | ||
| {{- if kindIs "map" . }} | ||
| {{- toYaml . | nindent 2 }} | ||
| {{- else }} | ||
| value: {{ . | toString | quote }} | ||
| {{- end }} | ||
| {{- end }} | ||
| {{- $rotation := .Values.global.auth.sessionKeyRotation -}} | ||
| {{- if and $rotation.id $rotation.phase }} | ||
| - name: WANDB_SESSION_KEY_ROTATION | ||
| value: {{ printf "%s-%s" $rotation.id $rotation.phase | quote }} | ||
| {{- end }} | ||
| {{- with .Values.global.auth.sessionKeyRolloutId }} | ||
| - name: WANDB_SESSION_KEY_ROLLOUT | ||
| value: {{ . | toString | quote }} | ||
| {{- end }} | ||
| {{- end -}} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,96 @@ | ||
| {{- $rotation := .Values.global.auth.sessionKeyRotation -}} | ||
| {{- $rotationID := $rotation.id | default "" -}} | ||
| {{- $rotationPhase := $rotation.phase | default "" -}} | ||
| {{- if ne (empty $rotationID) (empty $rotationPhase) -}} | ||
| {{- fail "global.auth.sessionKeyRotation.id and phase must be set together" -}} | ||
| {{- end -}} | ||
| {{- if and $rotationID (not (has $rotationPhase (list "prepare" "activate" "clear" "hard-cutover"))) -}} | ||
| {{- fail "global.auth.sessionKeyRotation.phase must be one of prepare, activate, clear, or hard-cutover" -}} | ||
| {{- end -}} | ||
| {{- if and $rotationID (or (not (empty .Values.global.auth.sessionKey)) (not (empty .Values.global.auth.sessionKeyPrevious))) -}} | ||
| {{- fail "global.auth.sessionKeyRotation cannot be combined with sessionKey or sessionKeyPrevious" -}} | ||
| {{- end -}} | ||
|
|
||
| {{- $secretName := printf "%s-gorilla-session-key" .Release.Name -}} | ||
| {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace $secretName) | default dict -}} | ||
| {{- $secretData := (get $secretObj "data") | default dict -}} | ||
| {{- $secretMetadata := (get $secretObj "metadata") | default dict -}} | ||
| {{- $secretAnnotations := (get $secretMetadata "annotations") | default dict -}} | ||
| {{- $storedRotationID := (get $secretAnnotations "wandb.ai/session-key-rotation-id") | default "" -}} | ||
| {{- $storedRotationPhase := (get $secretAnnotations "wandb.ai/session-key-rotation-phase") | default "" -}} | ||
| {{- $currentKey := (get $secretData "GORILLA_SESSION_KEY") | default (randAlphaNum 32 | b64enc) -}} | ||
| {{- $previousKeys := (get $secretData "GORILLA_SESSION_KEY_PREVIOUS") | default "" -}} | ||
|
|
||
| {{- if $rotationID -}} | ||
| {{- if eq $rotationPhase "hard-cutover" -}} | ||
| {{- if not (and (eq $storedRotationID $rotationID) (eq $storedRotationPhase "hard-cutover")) -}} | ||
| {{- if eq $storedRotationID $rotationID -}} | ||
| {{- fail "global.auth.sessionKeyRotation.id must be unique for each rotation" -}} | ||
| {{- end -}} | ||
| {{- $currentKey = randAlphaNum 32 | b64enc -}} | ||
| {{- end -}} | ||
| {{- $previousKeys = "" -}} | ||
| {{- else if eq $rotationPhase "prepare" -}} | ||
| {{- if and (eq $storedRotationID $rotationID) (eq $storedRotationPhase "prepare") -}} | ||
| {{- /* Reuse the candidate generated by the first prepare reconciliation. */ -}} | ||
| {{- else -}} | ||
| {{- if and $storedRotationPhase (not (has $storedRotationPhase (list "clear" "hard-cutover"))) -}} | ||
| {{- fail "a session key rotation is already in progress; clear it before starting another rotation" -}} | ||
| {{- end -}} | ||
| {{- if and (eq $storedRotationID $rotationID) (has $storedRotationPhase (list "clear" "hard-cutover")) -}} | ||
| {{- fail "global.auth.sessionKeyRotation.id must be unique for each rotation" -}} | ||
| {{- end -}} | ||
| {{- $candidateKey := randAlphaNum 32 -}} | ||
| {{- $previousKeyList := $candidateKey -}} | ||
| {{- if $previousKeys -}} | ||
| {{- $previousKeyList = printf "%s,%s" $candidateKey ($previousKeys | b64dec) -}} | ||
| {{- end -}} | ||
| {{- $previousKeys = $previousKeyList | b64enc -}} | ||
| {{- end -}} | ||
| {{- else if eq $rotationPhase "activate" -}} | ||
| {{- if or (ne $storedRotationID $rotationID) (not (has $storedRotationPhase (list "prepare" "activate"))) -}} | ||
| {{- fail "global.auth.sessionKeyRotation.phase=activate requires a prepared rotation with the same id" -}} | ||
| {{- end -}} | ||
| {{- if eq $storedRotationPhase "prepare" -}} | ||
| {{- $verificationKeys := splitList "," ($previousKeys | b64dec) -}} | ||
| {{- if empty $verificationKeys -}} | ||
| {{- fail "prepared session key rotation has no candidate key" -}} | ||
| {{- end -}} | ||
| {{- $candidateKey := first $verificationKeys -}} | ||
| {{- $outgoingKeys := list ($currentKey | b64dec) -}} | ||
| {{- if gt (len $verificationKeys) 1 -}} | ||
| {{- $outgoingKeys = concat $outgoingKeys (rest $verificationKeys) -}} | ||
| {{- end -}} | ||
| {{- $currentKey = $candidateKey | b64enc -}} | ||
| {{- $previousKeys = join "," $outgoingKeys | b64enc -}} | ||
| {{- end -}} | ||
| {{- else if eq $rotationPhase "clear" -}} | ||
| {{- if or (ne $storedRotationID $rotationID) (not (has $storedRotationPhase (list "prepare" "activate" "clear"))) -}} | ||
| {{- fail "global.auth.sessionKeyRotation.phase=clear requires a prepared or activated rotation with the same id" -}} | ||
| {{- end -}} | ||
| {{- $previousKeys = "" -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
|
|
||
| apiVersion: v1 | ||
| kind: Secret | ||
| metadata: | ||
| name: {{ .Release.Name }}-gorilla-session-key | ||
| name: {{ $secretName }} | ||
| annotations: | ||
| "helm.sh/resource-policy": "keep" | ||
| {{- if $rotationID }} | ||
| "wandb.ai/session-key-rotation-id": {{ $rotationID | quote }} | ||
| "wandb.ai/session-key-rotation-phase": {{ $rotationPhase | quote }} | ||
| {{- end }} | ||
| labels: | ||
| {{- include "wandb.commonLabels" . | nindent 4 }} | ||
| type: Opaque | ||
| data: | ||
| # Retrieve the secret data using lookup function and when not exists, return an empty dictionary / map as result | ||
| {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace (printf "%s-gorilla-session-key" .Release.Name)) | default dict }} | ||
| {{- $secretData := (get $secretObj "data") | default dict }} | ||
| # Set $gorillaSessionKey to existing secret data or generate a random one when not exists | ||
| {{- $gorillaSessionKey := (get $secretData "GORILLA_SESSION_KEY") | default (randAlphaNum 32 | b64enc) }} | ||
| GORILLA_SESSION_KEY: {{ $gorillaSessionKey | quote }} | ||
| GORILLA_SESSION_KEY: {{ $currentKey | quote }} | ||
| {{- if $previousKeys }} | ||
| GORILLA_SESSION_KEY_PREVIOUS: {{ $previousKeys | quote }} | ||
| {{- end }} | ||
| {{- $jwkUrl := ( .Values.global.auth.jwkUrl | default (printf "http://%s-app:8083/api/jwks.json" .Release.Name)) | b64enc }} | ||
| GORILLA_AUTH_JWK_URL: {{ $jwkUrl | quote }} | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.