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
Member

Step 1 of 2 to port the Cloudflare rotators. Adds the cloudflare-roll binary mode; the manifest port follows once CI publishes the image (the CronJobs pin it by digest, which doesn't exist until this merges).

What it does

Rolls a Cloudflare API token's value in place via PUT /user/tokens/{id}/value — same id, scope and policy, new value. No make-before-break, no destructive window.

Replaces the two identical python rotators, which differ only in two parameters:

bootstrap token-id field target
cloudflare (cert-manager) token-id infrastructure/cloudflare#api-token
external-dns 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 rolling independently.

Safety carried over from the earlier lessons

  • assert_fields_exist guards the target path — a typo strands the real token instead of adding a stray field.
  • The API value is used verbatim from serde_json with nothing appended: a stray newline is what once crash-looped external-dns (invalid header field value for Authorization).
  • result is accepted as a bare string or {value: ...}, so a future API shape change fails loud rather than storing "null".

Validated against real Cloudflare (throwaway token, zero blast radius)

PUT /user/tokens/{id}/value  {}   -> success: true, result is a bare string
new != old: YES,  no trailing newline: YES
new value verifies: active
old value verifies: Invalid API Token   <- roll invalidates old immediately (no window)

Throwaway token deleted after. clippy -D warnings clean; 12 tests pass; the in-cluster pieces (login, read_field, assert_fields_exist, patch_field) are the same code already proven live by the other modes.

🤖 Generated with Claude Code

Step 1 of 2 to port the Cloudflare rotators. Adds the `cloudflare-roll` binary mode; the manifest port follows once CI publishes the image (the CronJobs pin it by digest, which doesn't exist until this merges). ## What it does Rolls a Cloudflare API token's value in place via `PUT /user/tokens/{id}/value` — same id, scope and policy, new value. **No make-before-break, no destructive window.** Replaces the two identical python rotators, which differ only in two parameters: | | bootstrap token-id field | target | |---|---|---| | cloudflare (cert-manager) | `token-id` | `infrastructure/cloudflare#api-token` | | external-dns | `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 rolling independently. ## Safety carried over from the earlier lessons - `assert_fields_exist` guards the target path — a typo strands the real token instead of adding a stray field. - The API value is used **verbatim** from `serde_json` with nothing appended: a stray newline is what once crash-looped external-dns (`invalid header field value for Authorization`). - `result` is accepted as a bare string **or** `{value: ...}`, so a future API shape change fails loud rather than storing "null". ## Validated against real Cloudflare (throwaway token, zero blast radius) ``` PUT /user/tokens/{id}/value {} -> success: true, result is a bare string new != old: YES, no trailing newline: YES new value verifies: active old value verifies: Invalid API Token <- roll invalidates old immediately (no window) ``` Throwaway token deleted after. `clippy -D warnings` clean; 12 tests pass; the in-cluster pieces (login, read_field, assert_fields_exist, patch_field) are the same code already proven live by the other modes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-07-21 14:58:42 +00:00
feat(secret-rotator): cloudflare-roll mode
ci/woodpecker/push/build-ci-images Pipeline was successful
d88c4a6cd9
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>
k.renner merged commit f15a47db30 into master 2026-07-21 14:59:16 +00:00
Sign in to join this conversation.