Repository navigation
docs(runbooks): the connector-secret ring rotation order (append, flip, rewrap, drop) - #1902
Merged
Merged
Conversation
…p, rewrap, drop) connector-ops found the gap (74229): the two-phase rotation order lives only in the header of `backend/scripts/rewrap-connector-secrets.ts`, which nobody opens before editing a Secret Manager value. Add it to the runbook that operators actually read before touching these values. New §5 gives the order, the one-line rule the rest follows from — the ring value is appended to, never replaced — and the in-pod dry-run/`--apply` invocation. Plus §4 gains a step pointing at it. Facts read before writing, none assumed: - `rewrap()` (`connectorSecrets.ts:164-172`) returns early when a row is already under the active key and compare-and-sets each rewrite — so re-running is safe, and the old key must stay in the ring until the count reaches zero. - `parseKeyRing()` (`:49-79`) refuses an unset var, a duplicate id, or an active id absent from the ring, which is what makes "flip before append" a failure. - `node dist/scripts/rewrap-connector-secrets.js` is real in the image: `tsconfig.build.json` includes `scripts/**` (`include: **/*.ts`, not excluded) and the Dockerfile runs `npx tsc -p tsconfig.build.json`. - Both ring env refs are `optional: true` (`templates/core/backend-deployment.yaml`), and `api-keys` maps both at `latest` with no `version:` — the two properties that make a dropped ring quiet at boot and a destroyed version irreversible. No code, no values; follows the page's existing `-n <namespace>` convention.
… 2 needs a pod check (Vera's hold)
Vera's hold at `5c34249b` found a window the first draft walked straight into: if
step 3 runs while the pod still holds `ACTIVE_KEY=k1`, its selector
`{ keyId: { $ne: activeKeyId } }` matches nothing, so the run prints
`rewrapped 0; remaining under non-active keys: 0` — the documented acceptance
line, satisfied by "nothing needed it" rather than by a completed rewrap. The
operator would then drop `k1` and the rows naming it become unreadable.
- Step 2 now ends with a pod-side confirmation
(`printenv CONNECTOR_SECRET_ACTIVE_KEY` must read `k2`) and says why: until it
does, step 3 reports success for that reason.
- Step 3's acceptance is two numbers: the dry run's count is the pre-state, and
the apply line must read `rewrapped <that count>; remaining under non-active
keys: 0`. A `rewrapped 0` against a non-zero count sends the operator back to
step 2.
- Residual from 74233 folded in: the dry run selects on `keyId` and never
decrypts, so a row whose key has already left the ring still reads as merely
"needing rewrap". It surfaces only at `--apply`, which decrypts before it
writes (`getKey` throws `ConnectorSecretKeyMissing`) — fail-safe, nothing
written, but not evidence the old key is present.
- §0.1 contrast added as Vera asked: its freeze is for a *missing* `remoteRef`,
not for an `optional: true` ref that resolves to nothing.
Verified in `rewrap-connector-secrets.ts` before writing: the selector, the
`.select(...).lean()` with no decrypt, both printed strings, and that `rewrap()`
is the only thing that calls `decrypt`. Vera measured the piece I could not —
`dist/scripts/rewrap-connector-secrets.js` is present in the serving pod and the
dry run prints `connector secrets needing rewrap: 0`.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What
docs/runbooks/connector-credentials-setup.mdgains §5 Rotating the connector-secret key ring, plus a step in §4 pointing at it. Docs only, no code, no values.The gap, found by connector-ops (74229): the two-phase rotation order exists only in the header of
backend/scripts/rewrap-connector-secrets.ts, which nobody opens before editing a Secret Manager value. A rotation that replaces the ring value instead of appending to it drops the old key while rows still name it — those rows then stop decrypting, and the connector that needs one fails at use, not at boot.What §5 says
The rule the order follows from, on one line: the ring value is appended to, never replaced.
The four steps, in order: append the new key (leave
ACTIVE_KEYalone) → flipACTIVE_KEYto the new id (old key still in the ring) → rewrap until the script reportsremaining under non-active keys: 0→ only then drop the old key.The in-pod invocation, both halves:
Two quiet properties: both env refs are
optional: true, so a dropped ring starts the pod with no ring instead of refusing to start; andapi-keysmaps both atlatest, so destroying a version is the one irreversible act.Verified before writing (not assumed)
rewrap()returns early when a row is already under the active key; rewrites are compare-and-setbackend/services/connectorSecrets.ts:164-172parseKeyRing()refuses an unset var, a duplicate id, or an active id absent from the ringbackend/services/connectorSecrets.ts:49-79node dist/scripts/rewrap-connector-secrets.jsexists in the imagetsconfig.build.jsonincludesscripts/**(excludes only tests);Dockerfilerunsnpx tsc -p tsconfig.build.jsonoptional: truek8s/helm/commonly/templates/core/backend-deployment.yamlapi-keysmaps both atlatest,refreshInterval: 1hk8s/helm/commonly/templates/secrets/api-keys.yamlGate
@vera — the places to spend scepticism: the four-step order (is there a window where a row is unreadable?), and the
dist/scripts/...invocation (does the deployed image really carry it, and is the working directory right forkubectl exec?). Everything else is prose from the code above.Also note where this sits: the ring is the substrate of the just-merged #1898 (
connector_secretsrows keyed by(integrationId, kind)), so a wrong rotation would take those credentials with it — the reason the order is documented rather than left in a script header.Hold round (Vera 74235) — head moved
5c34249b→588149dbVera's hold found the window the first draft walked into, and it is the same question I had asked her to press. If step 3 runs while the pod still holds
ACTIVE_KEY=k1, the script's selector{ keyId: { $ne: activeKeyId } }matches nothing, so the run printsrewrapped 0; remaining under non-active keys: 0— the documented acceptance line, satisfied by "nothing needed it" rather than by a completed rewrap. The operator would then dropk1and the rows naming it become unreadable.Applied, exactly her list:
printenv CONNECTOR_SECRET_ACTIVE_KEYmust readk2— and says why: until it does, step 3 reports success for that reason. Added with it: that variable is the key id, a label, andCONNECTOR_SECRET_KEYSis never printed.rewrapped <that count>; remaining under non-active keys: 0. Arewrapped 0against a non-zero dry-run count means the pod is still on the old active id.keyIdand never decrypts, so a row whose key has already left the ring still reads as merely "needing rewrap". It surfaces only when--applycallsrewrap(), which decrypts before it writes (getKeythrowsConnectorSecretKeyMissing) — fail-safe, nothing written, but not evidence the old key is present.remoteRef; anoptional: trueref that resolves to nothing is simply absent, which is why this pair goes quiet instead of failing the sync.Checked against
backend/scripts/rewrap-connector-secrets.tsbefore writing: the{ keyId: { $ne: activeKeyId } }selector, the.select('_id keyId integrationId').lean()that never decrypts, both printed strings, and thatdecryptis reached only fromrewrap. Vera supplied the measurement I could not take:dist/scripts/rewrap-connector-secrets.jsis present in the serving pod (2365 bytes), the invocation runs, the dry run printsconnector secrets needing rewrap: 0, andls dist/scripts/no-such-script.jsfails as the negative control.One file, +33/−4 on top of the held head; nothing else moved.