feat(secret-rotator): cloudflare-roll mode #331

Merged
k.renner merged 1 commits from feat/cloudflare-roll-mode into master 2026-07-21 14:59:16 +00:00
1 Commits
Author SHA1 Message Date
claude-botandClaude Opus 4.8 d88c4a6cd9 feat(secret-rotator): cloudflare-roll mode
ci/woodpecker/push/build-ci-images Pipeline was successful
Adds the mode that will replace the two identical Cloudflare python rotators
(cert-manager's token and external-dns's). They differ only in which bootstrap
field holds the token id and which OpenBao path receives the value:

  cloudflare   bootstrap#token-id              -> infrastructure/cloudflare#api-token
  external-dns bootstrap#external-dns-token-id -> apps/external-dns#api-token

Both read the admin token from the same shared bootstrap, so it lives in one
place; only the token-id pin differs, keeping the two tokens rolling
independently.

Rolls the token value in place via PUT /user/tokens/{id}/value — same id,
scope and policy, new value — so there is no make-before-break and no
destructive window. assert_fields_exist guards the target path (a typo would
strand the real token), and the API value is used verbatim: a stray newline
would surface as "invalid header field value for Authorization" and crash the
consumer (the bug that hit external-dns once), so the string comes straight
from serde_json with nothing appended.

Handles result as either a bare string or {value: ...} so a future API shape
change fails loud rather than storing "null".

Validated against the real Cloudflare API on a throwaway token (zero blast
radius): PUT with empty body returns success + a bare-string value, new != old
with no trailing newline, new verifies active while the old immediately reads
"Invalid API Token" (confirming roll-in-place has no window). clippy
-D warnings clean; 12 tests pass.

No rotator uses it yet — the manifest port follows once CI publishes the image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:58:40 +02:00