refactor(rotation): port the five Class-A rotators to the secret-rotator binary #328

Merged
k.renner merged 1 commits from feat/port-class-a-rotators into master 2026-07-21 14:47:24 +00:00
Member

Ports paperless, woodpecker, shelly-manager, authentik and crowdsec-bouncer-key from inline shell to the secret-rotator binary's random mode. This is the payoff the binary was built for — five copies of the same bao kv patch boilerplate collapse to one tested, digest-pinned image.

Net -49 lines. Removes the OpenBao-login shell (duplicated in all five) and the busybox/openbao runtime image.

Behaviour preserved exactly

preserved
output bytes STANDARD base64 w/ padding, RFC 4648 unit-tested; 50 bytes for the Django keys (paperless, authentik), 32 for the rest
co-located fields merge-patch keeps WOODPECKER_GITEA_*, authentik smtp-password
shelly-manager admin-password + api-key still land in one patch, no split state
auth same k8s-auth roles / SAs / schedule; roles already grant read+patch

New safety property the shell lacked

assert_fields_exist refuses to run if a target field is absent — a typo becomes a loud failure with the old secret untouched, instead of a silent no-op that adds a stray field.

Testing

The random mode + assert_fields_exist are new code (only ha-token had run live). Tested against real OpenBao on a throwaway path + role — zero blast radius, no live consumer touched:

  • happy path: two fields rotated to 44-char values (32-byte base64), the co-located keep-me field UNTOUCHED, multi-field single patch (the shelly-manager shape)
  • fail-safe: a nonexistent target field → job Failed, error names the missing field and lists what's present, and the real field is unchanged (whole patch aborted, no partial write)

Static-verified that all five real paths currently hold their target fields, and all five pass server-side admission dry-run under the restricted PSA.

I did not trigger a live rotation of any of the five — each has real user-facing effects (paperless/authentik log users out; woodpecker/crowdsec restart) and there was an active CI pipeline. The scratch test proves the mechanism; the quarterly schedule (next: Oct 1, 04:00) exercises them naturally. Image digest sha256:159e5888….

🤖 Generated with Claude Code

Ports paperless, woodpecker, shelly-manager, authentik and crowdsec-bouncer-key from inline shell to the `secret-rotator` binary's `random` mode. This is the payoff the binary was built for — five copies of the same `bao kv patch` boilerplate collapse to one tested, digest-pinned image. **Net -49 lines.** Removes the OpenBao-login shell (duplicated in all five) and the busybox/openbao runtime image. ## Behaviour preserved exactly | | preserved | |---|---| | output bytes | STANDARD base64 w/ padding, RFC 4648 unit-tested; 50 bytes for the Django keys (paperless, authentik), 32 for the rest | | co-located fields | merge-patch keeps `WOODPECKER_GITEA_*`, authentik `smtp-password` | | shelly-manager | `admin-password` + `api-key` still land in **one** patch, no split state | | auth | same k8s-auth roles / SAs / schedule; roles already grant read+patch | ## New safety property the shell lacked `assert_fields_exist` refuses to run if a target field is absent — a typo becomes a **loud failure with the old secret untouched**, instead of a silent no-op that adds a stray field. ## Testing The `random` mode + `assert_fields_exist` are new code (only `ha-token` had run live). Tested against **real OpenBao on a throwaway path + role — zero blast radius, no live consumer touched**: - **happy path**: two fields rotated to 44-char values (32-byte base64), the co-located `keep-me` field **UNTOUCHED**, multi-field single patch (the shelly-manager shape) - **fail-safe**: a nonexistent target field → job **Failed**, error names the missing field and lists what's present, and the real field is **unchanged** (whole patch aborted, no partial write) Static-verified that all five real paths currently hold their target fields, and all five pass **server-side admission dry-run** under the restricted PSA. I did **not** trigger a live rotation of any of the five — each has real user-facing effects (paperless/authentik log users out; woodpecker/crowdsec restart) and there was an active CI pipeline. The scratch test proves the mechanism; the quarterly schedule (next: Oct 1, 04:00) exercises them naturally. Image digest `sha256:159e5888…`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-07-21 14:44:13 +00:00
Replaces the inline `head -c N /dev/urandom | base64 | tr -d '\n'` +
`bao kv patch` shell in each of paperless, woodpecker, shelly-manager,
authentik and crowdsec-bouncer-key with the secret-rotator binary's `random`
mode. Net -49 lines; the OpenBao login boilerplate (duplicated in all five)
and the busybox/openbao runtime image go away.

Behaviour preserved exactly:
  - byte-compatible output — N raw bytes, STANDARD base64 with padding (RFC
    4648 unit-tested); 50 bytes for the Django keys (paperless, authentik),
    32 for the rest
  - merge-patch, so co-located fields are untouched (WOODPECKER_GITEA_*,
    authentik smtp-password)
  - shelly-manager's two fields (admin-password, api-key) still land in ONE
    patch, no split state
  - same k8s-auth roles, SAs, schedule, quarterly timing — the roles already
    grant read+patch, which the binary's assert_fields_exist needs

Plus a new safety property the shell lacked: assert_fields_exist refuses to
run if a target field is absent, turning a typo into a loud failure with the
old secret untouched, instead of a silent no-op that adds a stray field.

Image pinned by digest sha256:159e5888… (all three ha-token fixes; the shared
OpenBao code path is the same one `random` uses).

Tested against real OpenBao on a throwaway path+role (zero blast radius, no
live consumer touched):
  - happy path: two fields rotated to 44-char values (32-byte base64),
    co-located field preserved, multi-field single patch
  - fail-safe: a nonexistent target field -> job Failed, clear error, the real
    field left unchanged (whole patch aborted, no partial write)
All five pass server-side admission dry-run under the restricted PSA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
k.renner merged commit 7320342d15 into master 2026-07-21 14:47:24 +00:00
Sign in to join this conversation.