Repository navigation
feat: run token rotation as one restate journal with only ciphertext journaled - #378
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
fuse | 8cc53a7 | Commit Preview URL Branch Preview URL |
Nov 15 2026, 07:37 AM |
| if err := uploadFiles(ctx, env, fusedCredentialFiles(creds)); err != nil { | ||
| return fmt.Errorf("upload credentials: %w", err) | ||
| } | ||
| setTokenIfSupported(env, creds) |
There was a problem hiding this comment.
Retries can skip credential files
After a partial upload, retrying this step can report success without writing every credential file. Run gives requests numbered keys, but uploadFiles assigns those numbers through concurrent goroutines over a map. A retry can assign /1 to a different file. The host agent compares only the HTTP method and URL, which are identical for all three uploads, so it returns the earlier file's cached success without writing the requested file.
Rotation can then persist a token that never reached the guest. Give each credential path a stable request key across attempts; sorting the map alone does not fix the race between concurrent requests.
| return runStep(ctx, opRotateToken, "persist", func(c context.Context) error { | ||
| return rotatePermanent(fleet.RotateTokenPersist(c, vmID, rotated)) | ||
| }, retry...) |
There was a problem hiding this comment.
If Postgres stays unavailable through the configured retry budget, this persist step ends with a final failure even though the guest already has the new credentials. Nothing schedules the same token write again. A later orchestrator restart loads the old token from Postgres while the guest's files hold the new one, leaving the crash-safety fix incomplete.
Keep the same pending token recoverable until it is saved, rather than ending the rotation when these retries run out.
8015771 to
bc5e78f
Compare
…phertext, with a durable rotator hook and rotate_token op metric
…e persist and a journal read for plaintext
…eout and keep server-side refusals unexported
…ied step keys each file as before
…st holds the new token
bc5e78f to
8cc53a7
Compare
Runs token rotation on the restate
Environment/{vm_id}object, so a crash between the guest upload and the state store write no longer leaves the stored token different from the guest's. #3 of the restate stack (#346 -> #345 -> #349 -> #348 -> #342).Environment/{vm_id}.RotateToken: generate ->Runupload ->Runpersist. generate is aRun, so a replay reuses the token it already made instead of generating a second one.RotatedCredentials: the token, cert and key sealed underTOKEN_ENCRYPTION_KEY, plus the token as the vm row stores it). the upload and persist steps decrypt inside themselves, so neither the plaintext token nor the tls key enters the journal.<step key>/1..3).persistVMBackgroundon this path. it also sets the token back on the environment handle, which a restart between upload and persist leaves on the old one.durableWaitfor an approved hitl run) bounds it.orchestrator_operations_total{op="rotate_token",result}and the duration histogram, in both modes; step metrics asrotate_token.{generate,upload,persist}.rotate-tokenapi action and an approved hitlrotate-tokenboth callFleetManager.RotateToken. fork and migrate still mint their own credentials in process; nothing here touches them.FUSE_RESTATE_MODE=offkeeps the in-process path (same steps, background persist as before).No api surface change (fields, states, status codes, messages), so no sdk/openapi changes. Docs:
docs/runbooks/restate.mdx(handler, journal query, key-change note),docs/content/docs/cloud/orchestrator-deploy.mdx(retry policy row, what runs on restate),docs/content/docs/cloud/monitoring.mdx(rotate_tokenop). Checked and left alone:concepts/environments.mdx,cloud/security.mdx, the clirotate-tokenpage, openapi (behavior unchanged).verified locally
gofmt -l .,go build ./...,go vet ./..., golangci-lint v2.12.2 (0 issues),go test ./... -count=1,-raceon durable/orchestrator.sys_journaljoined tosys_invocation), asserts it holds the generate step's ciphertext (so the check is not vacuous) and does not hold the plaintext token. a mutation returning the plaintext token from generate makes that test fail.bun run build;bun run lintfails only on pre-existing biome formatting in files this PR does not touch.not run
kill -9of the orchestrator between upload and persist, then exec into the vm, andrestate sqloversys_journalon a real rotation.Stacked on #377.
Closes #349