feat(secret-rotator): sync bot tokens into Woodpecker org secrets #363

Merged
k.renner merged 2 commits from claude-bot/rotator-woodpecker-secrets into master 2026-07-23 11:24:00 +00:00
Member

Moves review_bot_token and claude_code_oauth_token off hand-loading and into the secret-rotation toolchain, targeting org-level Woodpecker secrets (every repo under k.renner inherits them — no per-repo copies).

What it adds

  • secret-rotator woodpecker-secret mode (woodpecker.rs): upserts an OpenBao-held value into a k.renner org CI secret (create if absent, else patch value+events) via a dedicated woodpecker-bot bearer token. Verified endpoints against the live server (org id lookup, 404=absent). Never logs the value; refuses to push a blank over a working secret.
  • review-bot.yaml — one CronJob: init step mints review-bot's PAT (existing gitea-token mode, pull-verified against a private repo it collaborates on; push not verified — read collaborator, posts reviews via API), main step syncs it to org secret review_bot_token (cron,manual,pull_request).
  • claude-oauth.yaml — sync-only (interactive setup-token can't be minted headlessly): daily push of an owner-seeded OpenBao value → org secret claude_code_oauth_token.
  • Policies rotator-{review-bot,claude-oauth}.hcl (least-privilege) + netpol (rotator-*-egress, gitea-allow-review-bot-rotator, new woodpecker-allow-rotators).

Validation

  • cargo test green (15 tests incl. the new module); kubectl kustomize builds clean for the rotation dir, woodpecker app, and network-policies.

Deploy prerequisites (can't come from git — do before/at rollout)

  • Build a secret-rotator image including woodpecker-secret mode, then pin @sha256 in review-bot.yaml + claude-oauth.yaml (they're on :latest; the digest the other rotators pin predates this mode and 404s with "unknown mode").
  • Apply the two OpenBao policies + k8s auth roles (commands in the .hcl headers).
  • Seed bootstraps: secret-rotators-bootstrap/woodpecker-bot#token (a dedicated woodpecker-bot API token, org-secret write), secret-rotators-bootstrap/review-bot (review-bot's password), infrastructure/review-bot#token (placeholder), infrastructure/claude-code-oauth#token (your setup-token value).
  • Trigger the two CronJobs once (kubectl -n openbao create job --from=cronjob/...), confirm the org secrets appear, then drop homelab's repo-level claude_code_oauth_token / review_bot_token so the org versions are the single source.

Depends on / completes the PR-event review-bot cutover (k.renner/homelab#362 + the shelly/authentik/HA PRs).

Moves `review_bot_token` and `claude_code_oauth_token` off hand-loading and into the **secret-rotation toolchain**, targeting **org-level** Woodpecker secrets (every repo under `k.renner` inherits them — no per-repo copies). ## What it adds - **`secret-rotator` `woodpecker-secret` mode** (`woodpecker.rs`): upserts an OpenBao-held value into a `k.renner` **org** CI secret (create if absent, else patch value+events) via a dedicated `woodpecker-bot` bearer token. Verified endpoints against the live server (org id lookup, 404=absent). Never logs the value; refuses to push a blank over a working secret. - **`review-bot.yaml`** — one CronJob: init step mints review-bot's PAT (existing `gitea-token` mode, pull-verified against a *private* repo it collaborates on; push not verified — read collaborator, posts reviews via API), main step syncs it to org secret `review_bot_token` (`cron,manual,pull_request`). - **`claude-oauth.yaml`** — sync-only (interactive `setup-token` can't be minted headlessly): daily push of an owner-seeded OpenBao value → org secret `claude_code_oauth_token`. - **Policies** `rotator-{review-bot,claude-oauth}.hcl` (least-privilege) + **netpol** (`rotator-*-egress`, `gitea-allow-review-bot-rotator`, new `woodpecker-allow-rotators`). ## Validation - `cargo test` green (15 tests incl. the new module); `kubectl kustomize` builds clean for the rotation dir, woodpecker app, and network-policies. ## Deploy prerequisites (can't come from git — do before/at rollout) - [ ] Build a `secret-rotator` image including `woodpecker-secret` mode, then **pin `@sha256`** in `review-bot.yaml` + `claude-oauth.yaml` (they're on `:latest`; the digest the other rotators pin predates this mode and 404s with "unknown mode"). - [ ] Apply the two OpenBao policies + k8s auth roles (commands in the `.hcl` headers). - [ ] Seed bootstraps: `secret-rotators-bootstrap/woodpecker-bot#token` (a dedicated woodpecker-bot API token, org-secret write), `secret-rotators-bootstrap/review-bot` (review-bot's password), `infrastructure/review-bot#token` (placeholder), `infrastructure/claude-code-oauth#token` (your `setup-token` value). - [ ] Trigger the two CronJobs once (`kubectl -n openbao create job --from=cronjob/...`), confirm the org secrets appear, then **drop homelab's repo-level `claude_code_oauth_token` / `review_bot_token`** so the org versions are the single source. Depends on / completes the PR-event review-bot cutover (k.renner/homelab#362 + the shelly/authentik/HA PRs).
review-bot left a comment
Member

🤖 Automated review — model claude-opus-4-8 (complexity: complex), commit 121a07d496e244adc9abad742602db375b536f8f.

Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.


Summary

Adds a woodpecker-secret mode to secret-rotator (new woodpecker.rs) that
upserts an OpenBao-held value into a Woodpecker org-level CI secret, plus two
CronJobs (review-bot.yaml, claude-oauth.yaml), two least-privilege OpenBao
policies, and the Cilium ingress/egress halves to let the openbao-ns rotators
reach gitea :3000 and the woodpecker server :8000. The change is careful and
well-documented; I found no blocking issues — the notes below are
robustness/clarity suggestions.

I verified: the woodpecker-server pod carries the labels the new
woodpecker-allow-rotators ingress policy selects (server.yaml:7-8, port 8000
at server.yaml:94); DNS/apiserver/world egress for the new rotator pods is
already granted by the openbao default-deny-ingress endpointSelector: {}
policy (openbao-ns.yaml:37-84), so the new egress policies only need to add the
in-cluster targets — matching the existing rotator-gitea-flux-egress pattern;
the apps ingress model is additive (allow-ingress.yaml, default-deny.yaml),
so adding the rotator sources does not restrict Traefik/agent access; and the
OpenBao policy paths (homelab/data/infrastructure/...) match the data/
KV-v2 path bao.rs constructs.

🔴 Blocking

None.

🟡 Suggestions

  • is_success() branches are effectively unreachable in
    secret-rotator/src/woodpecker.rs upsert_org_secret (the PATCH block and the
    POST block). ureq 3 treats non-2xx as Err by default — the codebase relies
    on exactly this in gitea.rs:82-96 (Err(ureq::Error::StatusCode(403))). So
    .send_json(&body)? propagates a raw StatusCode error on failure and the
    else { err(format!("... -> {}", resp.status())) } arms never run. The
    intended, more useful message (create/patch org secret k.renner/... -> 500)
    will therefore never be emitted. Consider either matching on
    Err(ureq::Error::StatusCode(code)) like gitea.rs does, or dropping the dead
    else and letting the ureq error propagate — the current form reads as if it
    handles the failure path but doesn't.

  • PATCH omits images (woodpecker.rs, the exists()==true branch sends
    { "value": value, "events": events }). If Woodpecker's PATCH handler resets
    unspecified fields to defaults, an existing secret's images restriction would
    be widened to "all images" on the next sync — a small trust-boundary change for
    review_bot_token / claude_code_oauth_token. I could not confirm Woodpecker's
    patch-merge semantics from this repo. One check settles it: PATCH an org secret
    that has a non-empty images list and confirm the list survives; if it doesn't,
    send the existing images back in the body.

  • CronJobs ship suspend: false on a :latest image that does not yet contain
    this mode.
    claude-oauth.yaml:295 runs daily (schedule: "20 4 * * *") and
    review-bot.yaml quarterly, both on
    secret-rotator:latest (claude-oauth.yaml:325, review-bot.yaml:451/493).
    Per the PR body the current pinned digest 404s with "unknown mode", so once
    Flux applies these, the claude-oauth Job will fail daily until the image is
    rebuilt and @sha256-pinned. It fails safely (errors before any API write, old
    org secret untouched) and failedJobsHistoryLimit: 5 caps the litter, but it
    will generate recurring failed Jobs / alert noise. Consider shipping with
    suspend: true and flipping it in the same change that pins the digest, so the
    merge order can't produce failing Jobs.

🟢 Nits

  • review-bot.yaml uses app.kubernetes.io/name: woodpecker (no component) in
    the egress toEndpoints (via rotator-review-bot-egress), which also matches
    the agent pod. Harmless (traffic targets the server Service), but scoping to
    component: server would mirror the ingress side's tighter selector.

Tests

Consistent with the repo's convention: woodpecker.rs unit-tests only the pure
bearer() helper, exactly as gitea.rs tests only basic() — the HTTP paths are
not mocked anywhere in this crate. The create-vs-patch branching and org_id
JSON parsing are therefore exercised only against the live server (the PR notes a
manual endpoint verification). That's acceptable here, but the org_id parse
(v["id"].as_i64()) and the events-splitting in sync_woodpecker_secret are pure
and could be cheaply unit-tested if you want regression coverage without a server.

Recommendation

Looks reasonable — no blocking issues; comments above are optional.

🤖 **Automated review** — model `claude-opus-4-8` (complexity: complex), commit `121a07d496e244adc9abad742602db375b536f8f`. > Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review. --- ## Summary Adds a `woodpecker-secret` mode to `secret-rotator` (new `woodpecker.rs`) that upserts an OpenBao-held value into a Woodpecker **org-level** CI secret, plus two CronJobs (`review-bot.yaml`, `claude-oauth.yaml`), two least-privilege OpenBao policies, and the Cilium ingress/egress halves to let the openbao-ns rotators reach gitea `:3000` and the woodpecker server `:8000`. The change is careful and well-documented; I found **no blocking issues** — the notes below are robustness/clarity suggestions. I verified: the `woodpecker-server` pod carries the labels the new `woodpecker-allow-rotators` ingress policy selects (`server.yaml:7-8`, port 8000 at `server.yaml:94`); DNS/apiserver/world egress for the new rotator pods is already granted by the openbao `default-deny-ingress` `endpointSelector: {}` policy (`openbao-ns.yaml:37-84`), so the new egress policies only need to add the in-cluster targets — matching the existing `rotator-gitea-flux-egress` pattern; the apps ingress model is additive (`allow-ingress.yaml`, `default-deny.yaml`), so adding the rotator sources does not restrict Traefik/agent access; and the OpenBao policy paths (`homelab/data/infrastructure/...`) match the `data/` KV-v2 path `bao.rs` constructs. ## 🔴 Blocking None. ## 🟡 Suggestions - **`is_success()` branches are effectively unreachable** in `secret-rotator/src/woodpecker.rs` `upsert_org_secret` (the PATCH block and the POST block). `ureq` 3 treats non-2xx as `Err` by default — the codebase relies on exactly this in `gitea.rs:82-96` (`Err(ureq::Error::StatusCode(403))`). So `.send_json(&body)?` propagates a raw `StatusCode` error on failure and the `else { err(format!("... -> {}", resp.status())) }` arms never run. The intended, more useful message (`create/patch org secret k.renner/... -> 500`) will therefore never be emitted. Consider either matching on `Err(ureq::Error::StatusCode(code))` like `gitea.rs` does, or dropping the dead `else` and letting the ureq error propagate — the current form reads as if it handles the failure path but doesn't. - **PATCH omits `images`** (`woodpecker.rs`, the `exists()==true` branch sends `{ "value": value, "events": events }`). If Woodpecker's PATCH handler resets unspecified fields to defaults, an existing secret's `images` restriction would be widened to "all images" on the next sync — a small trust-boundary change for `review_bot_token` / `claude_code_oauth_token`. I could not confirm Woodpecker's patch-merge semantics from this repo. One check settles it: PATCH an org secret that has a non-empty `images` list and confirm the list survives; if it doesn't, send the existing `images` back in the body. - **CronJobs ship `suspend: false` on a `:latest` image that does not yet contain this mode.** `claude-oauth.yaml:295` runs daily (`schedule: "20 4 * * *"`) and `review-bot.yaml` quarterly, both on `secret-rotator:latest` (`claude-oauth.yaml:325`, `review-bot.yaml:451/493`). Per the PR body the current pinned digest 404s with "unknown mode", so once Flux applies these, the claude-oauth Job will fail daily until the image is rebuilt and `@sha256`-pinned. It fails safely (errors before any API write, old org secret untouched) and `failedJobsHistoryLimit: 5` caps the litter, but it will generate recurring failed Jobs / alert noise. Consider shipping with `suspend: true` and flipping it in the same change that pins the digest, so the merge order can't produce failing Jobs. ## 🟢 Nits - `review-bot.yaml` uses `app.kubernetes.io/name: woodpecker` (no `component`) in the egress `toEndpoints` (via `rotator-review-bot-egress`), which also matches the agent pod. Harmless (traffic targets the server Service), but scoping to `component: server` would mirror the ingress side's tighter selector. ## Tests Consistent with the repo's convention: `woodpecker.rs` unit-tests only the pure `bearer()` helper, exactly as `gitea.rs` tests only `basic()` — the HTTP paths are not mocked anywhere in this crate. The create-vs-patch branching and `org_id` JSON parsing are therefore exercised only against the live server (the PR notes a manual endpoint verification). That's acceptable here, but the `org_id` parse (`v["id"].as_i64()`) and the events-splitting in `sync_woodpecker_secret` are pure and could be cheaply unit-tested if you want regression coverage without a server. ## Recommendation Looks reasonable — no blocking issues; comments above are optional. <!-- pr-review:sha=121a07d496e244adc9abad742602db375b536f8f -->
claude-bot force-pushed claude-bot/rotator-woodpecker-secrets from 121a07d496 to 581f25d72c 2026-07-23 10:48:07 +00:00 Compare
claude-bot force-pushed claude-bot/rotator-woodpecker-secrets from 581f25d72c to 16013a4ca0 2026-07-23 10:52:59 +00:00 Compare
review-bot left a comment
Member

🤖 Automated review — model claude-opus-4-8 (complexity: complex), commit 16013a4ca051697d33b209b4ec4e47c560fd6f99.

Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.


Summary

Adds a woodpecker-secret mode to secret-rotator that upserts an OpenBao-held
value into a k.renner org-level Woodpecker CI secret, plus two CronJobs
(review-bot, claude-oauth), OpenBao policies, and the netpol halves to wire
them up. The Rust is clean, well-tested for its unit surface, and consistent
with the existing rotators; the design (org-level, sync-not-mint, value never
logged) is sound. No blocking defects found — the notes below are verification
and sequencing items the author has largely pre-flagged.

🟡 Suggestions

1. The pull-only verify gate depends on shelly-manager being private —
please confirm.
review-bot.yaml:473-482 sets GITEA_VERIFY_REPO: k.renner/shelly-manager with GITEA_REQUIRE_PUSH: "false". The existing
rotate_gitea_token code warns exactly about this at secret-rotator/src/main.rs:285-289:
for a public verify repo, git-upload-pack returns 200 for any valid token
regardless of scope, so a pull-only check degrades to "is this any valid token"
— the safety gate that keeps a broken PAT from overwriting a working one stops
being load-bearing. The manifest comment asserts the repo is private, but that
can't be verified from this checkout (the homelab repo itself is public per
pr.json). One check settles it: curl -so /dev/null -w '%{http_code}' https://gitea.renner.uno/k.renner/shelly-manager (302→login = private, 200 =
public). If it's public, either flip GITEA_REQUIRE_PUSH back on against a repo
review-bot can push, or point GITEA_VERIFY_REPO at a private repo.

2. :latest will fail nightly until the image is rebuilt and pinned.
claude-oauth.yaml:325 and review-bot.yaml:451,493 run
secret-rotator:latest with args: ["woodpecker-secret"]. The current :latest
predates this mode, so it exits with unknown mode 'woodpecker-secret'
(main.rs:41). The claude-oauth CronJob is suspend: false on a daily 20 4 * * * schedule, so once Flux applies this it fails every night at 04:20 until the
new image is built and the @sha256 pinned. This is non-destructive (a failed
sync leaves the org secret untouched) and the PR body lists it as a rollout
prerequisite — just calling out that the failure window opens at merge, not at
your convenience. Consider suspend: true on both CronJobs in this PR and
flipping them on in the image-pin follow-up, so a merge doesn't start a failing
schedule.

3. Make-before-break has a narrow reaping window if a sync fails.
review-bot.yaml: the init step stores the new PAT to OpenBao and reaps old
tokens keeping the two newest (main.rs:325-345), independent of whether the
main sync-woodpecker step then succeeds. If a quarter's sync step fails,
Woodpecker keeps referencing token T1 while OpenBao holds T2. The next
quarter mints T3 and reaps down to {T3, T2} — deleting T1, which Woodpecker is
still using, before T3's sync lands. Quarterly cadence + backoffLimit: 1 makes
this unlikely, but the "if the sync step fails the pipeline keeps working on the
prior value" claim in the header comment holds only for one cycle. Not worth
restructuring; worth a one-line caveat in the comment.

🟢 Nits

  • woodpecker.rs:715-719 / 728-732: the else { err(...) } arms after
    send_json(&body)? are effectively unreachable — ureq 3 has
    http_status_as_error on by default, so a non-2xx already returns Err from
    ?. Harmless and it mirrors the same defensive pattern in bao.rs:135, so
    fine to leave; just noting it isn't doing what the branch implies.

Tests

cargo test is reported green (15 tests). The new module ships a unit test for
bearer() (woodpecker.rs:736-743), consistent with how gitea.rs / bao.rs
test their pure helpers (basic(), mount()/sub()). The HTTP-driven logic
(org_id, exists's 404-means-absent branch, create-vs-patch selection) is not
unit-tested — matching the repo's existing convention of not mocking ureq — and
the author states it was verified against the live server. That's the right call
for this codebase; the one behavior I'd most want asserted is exists()
mapping only 404 to false and propagating every other status, since that's the
guard that stops a blind create/patch.

Recommendation

Looks reasonable — no blocking issues; comments above are optional (item 1 is a
verification worth doing before rollout).

🤖 **Automated review** — model `claude-opus-4-8` (complexity: complex), commit `16013a4ca051697d33b209b4ec4e47c560fd6f99`. > Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review. --- ## Summary Adds a `woodpecker-secret` mode to `secret-rotator` that upserts an OpenBao-held value into a `k.renner` org-level Woodpecker CI secret, plus two CronJobs (`review-bot`, `claude-oauth`), OpenBao policies, and the netpol halves to wire them up. The Rust is clean, well-tested for its unit surface, and consistent with the existing rotators; the design (org-level, sync-not-mint, value never logged) is sound. No blocking defects found — the notes below are verification and sequencing items the author has largely pre-flagged. ## 🟡 Suggestions **1. The pull-only verify gate depends on `shelly-manager` being private — please confirm.** `review-bot.yaml:473-482` sets `GITEA_VERIFY_REPO: k.renner/shelly-manager` with `GITEA_REQUIRE_PUSH: "false"`. The existing `rotate_gitea_token` code warns exactly about this at `secret-rotator/src/main.rs:285-289`: for a *public* verify repo, `git-upload-pack` returns 200 for any valid token regardless of scope, so a pull-only check degrades to "is this any valid token" — the safety gate that keeps a broken PAT from overwriting a working one stops being load-bearing. The manifest comment asserts the repo is private, but that can't be verified from this checkout (the homelab repo itself is public per `pr.json`). One check settles it: `curl -so /dev/null -w '%{http_code}' https://gitea.renner.uno/k.renner/shelly-manager` (302→login = private, 200 = public). If it's public, either flip `GITEA_REQUIRE_PUSH` back on against a repo review-bot can push, or point `GITEA_VERIFY_REPO` at a private repo. **2. `:latest` will fail nightly until the image is rebuilt and pinned.** `claude-oauth.yaml:325` and `review-bot.yaml:451,493` run `secret-rotator:latest` with `args: ["woodpecker-secret"]`. The current `:latest` predates this mode, so it exits with `unknown mode 'woodpecker-secret'` (`main.rs:41`). The claude-oauth CronJob is `suspend: false` on a daily `20 4 * * *` schedule, so once Flux applies this it fails every night at 04:20 until the new image is built and the `@sha256` pinned. This is non-destructive (a failed sync leaves the org secret untouched) and the PR body lists it as a rollout prerequisite — just calling out that the failure window opens at merge, not at your convenience. Consider `suspend: true` on both CronJobs in this PR and flipping them on in the image-pin follow-up, so a merge doesn't start a failing schedule. **3. Make-before-break has a narrow reaping window if a sync fails.** `review-bot.yaml`: the init step stores the new PAT to OpenBao and reaps old tokens keeping the two newest (`main.rs:325-345`), independent of whether the main `sync-woodpecker` step then succeeds. If a quarter's sync step fails, Woodpecker keeps referencing token T1 while OpenBao holds T2. The *next* quarter mints T3 and reaps down to {T3, T2} — deleting T1, which Woodpecker is still using, before T3's sync lands. Quarterly cadence + `backoffLimit: 1` makes this unlikely, but the "if the sync step fails the pipeline keeps working on the prior value" claim in the header comment holds only for one cycle. Not worth restructuring; worth a one-line caveat in the comment. ## 🟢 Nits - `woodpecker.rs:715-719` / `728-732`: the `else { err(...) }` arms after `send_json(&body)?` are effectively unreachable — ureq 3 has `http_status_as_error` on by default, so a non-2xx already returns `Err` from `?`. Harmless and it mirrors the same defensive pattern in `bao.rs:135`, so fine to leave; just noting it isn't doing what the branch implies. ## Tests `cargo test` is reported green (15 tests). The new module ships a unit test for `bearer()` (`woodpecker.rs:736-743`), consistent with how `gitea.rs` / `bao.rs` test their pure helpers (`basic()`, `mount()/sub()`). The HTTP-driven logic (`org_id`, `exists`'s 404-means-absent branch, create-vs-patch selection) is not unit-tested — matching the repo's existing convention of not mocking ureq — and the author states it was verified against the live server. That's the right call for this codebase; the one behavior I'd most want asserted is `exists()` mapping only 404 to `false` and propagating every other status, since that's the guard that stops a blind create/patch. ## Recommendation Looks reasonable — no blocking issues; comments above are optional (item 1 is a verification worth doing before rollout). <!-- pr-review:sha=16013a4ca051697d33b209b4ec4e47c560fd6f99 -->
claude-bot added 1 commit 2026-07-23 11:03:47 +00:00
Review on PR #363:
- Suspend both CronJobs at merge (review-bot quarterly, claude-oauth daily):
  the pinned image is still :latest and predates woodpecker-secret mode, so an
  un-suspended merge would fail on schedule with 'unknown mode'. The image-pin
  follow-up flips them on. (suggestion 2)
- GITEA_VERIFY_REPO: note shelly-manager verified private 2026-07-23 (unauth
  404 + Woodpecker SCM PRIVATE=yes) and the degradation-if-public risk, so the
  pull-only gate's assumption is documented. (suggestion 1)
- make-before-break header: caveat that the 'sync-fail keeps working' property
  holds for one cycle; a persistent sync failure must be fixed before the next
  quarterly run reaps the still-referenced token. (suggestion 3)

Left as-is per the review: the woodpecker.rs defensive else-arms (mirror
bao.rs) and no ureq-mocking test (repo convention).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
review-bot left a comment
Member

🤖 Automated review — model claude-opus-4-8 (complexity: complex), commit 0f41efee359af868811ff90e926a23f0513b5073.

Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.


Summary

Adds a woodpecker-secret mode to the secret-rotator binary (new woodpecker.rs)
that upserts an OpenBao-held value into a k.renner org-level Woodpecker CI
secret, plus two suspended CronJobs (review-bot.yaml, claude-oauth.yaml),
their OpenBao policies, and the Cilium ingress/egress halves. The change is
well-scoped, closely mirrors the existing flux-git rotator conventions, and both
CronJobs ship suspend: true so nothing runs at merge. No blocking issues found.

🟡 Suggestions

  • Org-scope widens the secret's blast radius vs. repo-scope —
    review-bot.yaml:534 / claude-oauth.yaml:351 push review_bot_token
    (a write:repository Gitea PAT) and claude_code_oauth_token to a k.renner
    org secret, inherited by every repo in the org, with pull_request in the
    event set. homelab itself is a public repo (pr.json → "private":false).
    Worth confirming Woodpecker does not hand these secrets to pipelines triggered
    by fork/untrusted pull_request events on any org repo before un-suspending —
    moving from repo- to org-scope only enlarges the set of pipelines that can read
    a write-scoped credential. This is the stated design intent, so just a
    double-check, not a defect.

  • GITEA_VERIFY_REPO visibility is load-bearing but external to this repo —
    review-bot.yaml:492 points the pull-verify at k.renner/shelly-manager and
    the comment itself flags that if that repo ever goes public the check silently
    degrades to "is this any valid token" (main.rs:285-289). The safeguard is a
    hand-maintained invariant with no automated guard; the inline IMPORTANT note
    is the right mitigation, but it's the kind of thing that rots. No action needed
    now.

  • read_field is the only guard against pushing a blank —
    main.rs:611-613 relies on bao::read_field erroring on missing/empty
    (confirmed at bao.rs:76-79), which is correct. Unlike rotate_gitea_token,
    the woodpecker-secret path has no assert_fields_exist — fine here since it
    never patches OpenBao, but note the "refuse blank over working secret" claim
    depends entirely on read_field's empty-check, so keep that invariant if
    read_field is ever refactored.

🟢 Nits

  • Dead else branches in upsert_org_secret — woodpecker.rs:733-737 and
    746-750 check resp.status().is_success() and build a custom
    "patch/create org secret … -> {status}" error otherwise. With ureq 3
    (Cargo.toml:11, http_status_as_error defaults to true, and the rest of
    the codebase relies on this — see gitea.rs:90-94), send_json already returns
    Err(StatusCode(..)) for any non-2xx, so those else arms are unreachable and
    the nicer message never fires; the raw ureq error propagates instead. Harmless,
    but inconsistent with exists() right above it, which correctly matches on
    Err(StatusCode(404)).

  • rotator-review-bot-egress woodpecker toEndpoints isn't scoped to component: server
    — openbao-ns.yaml:166-173 (and the claude-oauth egress at 189-196) allow
    egress to any app.kubernetes.io/name: woodpecker pod on :8000, whereas the
    apps-side ingress (woodpecker/networkpolicy.yaml:72-74) correctly narrows to
    component: server. Egress-side over-breadth is harmless (agents don't listen
    on 8000), just slightly looser than necessary.

Tests

Consistent with the repo's convention: the only unit test is
bearer_header_prefixes_token (woodpecker.rs:758-761). The create-vs-patch
branching in upsert_org_secret, the org_id lookup, and the 404→absent logic
in exists are unverified by tests — the same as gitea.rs/cloudflare.rs,
which only test pure helpers, so this matches the established pattern rather than
introducing a gap. The PR body reports these endpoints were verified against the
live server. If a test were added, the highest-value one would cover
exists returning false on 404 vs. propagating other status codes (that
behavior is what keeps a create/patch from firing blind).

Recommendation

Looks reasonable — no blocking issues; comments above are optional.

🤖 **Automated review** — model `claude-opus-4-8` (complexity: complex), commit `0f41efee359af868811ff90e926a23f0513b5073`. > Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review. --- ## Summary Adds a `woodpecker-secret` mode to the `secret-rotator` binary (new `woodpecker.rs`) that upserts an OpenBao-held value into a `k.renner` **org-level** Woodpecker CI secret, plus two suspended CronJobs (`review-bot.yaml`, `claude-oauth.yaml`), their OpenBao policies, and the Cilium ingress/egress halves. The change is well-scoped, closely mirrors the existing flux-git rotator conventions, and both CronJobs ship `suspend: true` so nothing runs at merge. No blocking issues found. ## 🟡 Suggestions - **Org-scope widens the secret's blast radius vs. repo-scope** — `review-bot.yaml:534` / `claude-oauth.yaml:351` push `review_bot_token` (a `write:repository` Gitea PAT) and `claude_code_oauth_token` to a `k.renner` **org** secret, inherited by *every* repo in the org, with `pull_request` in the event set. `homelab` itself is a public repo (`pr.json` → `"private":false`). Worth confirming Woodpecker does not hand these secrets to pipelines triggered by fork/untrusted `pull_request` events on any org repo before un-suspending — moving from repo- to org-scope only enlarges the set of pipelines that can read a write-scoped credential. This is the stated design intent, so just a double-check, not a defect. - **`GITEA_VERIFY_REPO` visibility is load-bearing but external to this repo** — `review-bot.yaml:492` points the pull-verify at `k.renner/shelly-manager` and the comment itself flags that if that repo ever goes public the check silently degrades to "is this any valid token" (`main.rs:285-289`). The safeguard is a hand-maintained invariant with no automated guard; the inline `IMPORTANT` note is the right mitigation, but it's the kind of thing that rots. No action needed now. - **`read_field` is the only guard against pushing a blank** — `main.rs:611-613` relies on `bao::read_field` erroring on missing/empty (confirmed at `bao.rs:76-79`), which is correct. Unlike `rotate_gitea_token`, the woodpecker-secret path has no `assert_fields_exist` — fine here since it never patches OpenBao, but note the "refuse blank over working secret" claim depends entirely on `read_field`'s empty-check, so keep that invariant if `read_field` is ever refactored. ## 🟢 Nits - **Dead `else` branches in `upsert_org_secret`** — `woodpecker.rs:733-737` and `746-750` check `resp.status().is_success()` and build a custom `"patch/create org secret … -> {status}"` error otherwise. With ureq 3 (`Cargo.toml:11`, `http_status_as_error` defaults to `true`, and the rest of the codebase relies on this — see `gitea.rs:90-94`), `send_json` already returns `Err(StatusCode(..))` for any non-2xx, so those `else` arms are unreachable and the nicer message never fires; the raw ureq error propagates instead. Harmless, but inconsistent with `exists()` right above it, which correctly matches on `Err(StatusCode(404))`. - **`rotator-review-bot-egress` woodpecker toEndpoints isn't scoped to `component: server`** — `openbao-ns.yaml:166-173` (and the claude-oauth egress at `189-196`) allow egress to any `app.kubernetes.io/name: woodpecker` pod on :8000, whereas the apps-side ingress (`woodpecker/networkpolicy.yaml:72-74`) correctly narrows to `component: server`. Egress-side over-breadth is harmless (agents don't listen on 8000), just slightly looser than necessary. ## Tests Consistent with the repo's convention: the only unit test is `bearer_header_prefixes_token` (`woodpecker.rs:758-761`). The create-vs-patch branching in `upsert_org_secret`, the `org_id` lookup, and the 404→absent logic in `exists` are unverified by tests — the same as `gitea.rs`/`cloudflare.rs`, which only test pure helpers, so this matches the established pattern rather than introducing a gap. The PR body reports these endpoints were verified against the live server. If a test were added, the highest-value one would cover `exists` returning `false` on 404 vs. propagating other status codes (that behavior is what keeps a create/patch from firing blind). ## Recommendation Looks reasonable — no blocking issues; comments above are optional. <!-- pr-review:sha=0f41efee359af868811ff90e926a23f0513b5073 -->
k.renner merged commit 6b578aa35b into master 2026-07-23 11:24:00 +00:00
Sign in to join this conversation.