Zot push credential for pipelines: secret-extension rule + AppRole path #110

Closed
opened 2026-09-29 16:27:37 -07:00 by tepichord · 1 comment
Owner

Why

annia-infra#310 ends in a step that pushes milner/annia:<tag> to zot, and that push cannot be authenticated today. The registry's access control is explicit about it:

  • ** — anonymous callers may read only (values.yaml, accessControl), so annia/** — where the new image goes — is not anonymously writable;
  • the only two entries that grant anonymous create/update are preview/** (deliberately permanent) and milner/** (deliberately temporary);
  • annia-ci is the one writer identity and can write **, and its password is secret/ci/zot → ZOT_TOKEN.

So the credential exists in Vault and is read by nobody: the secret extension's AppRole is granted secret/data/forgejo/token and secret/data/irc/herald and no other path, and it has no rule for tepichord/annia-infra on a push event, which means a step asking for ZOT_TOKEN would not even compile — SecretListPipeline refuses a name no rule serves. gitops/clusters/milner/infrastructure/zot/README.md says exactly this: "Nothing reads secret/ci/zot yet … that entry is written ahead of the rule that will serve it." This issue is the rule.

What this changes

Two halves, and they are one change because either alone is a no-op:

  1. apps/woodpecker/secret-extension.yaml — a third rule, and a new rule rather than a wider one on the existing merged-PR rule, because "a second consumer gets its own rule rather than a wider path, so this file stays a list of what the token is for": the archive-labels action has no business holding the registry password, and the publish step has no business holding the Forgejo token.
      - id: "annia-infra-publish"
        repo: "tepichord/annia-infra"
        # A push to main is the only thing that publishes, so nothing on a pull
        # request can obtain the registry credential — which is the same
        # direction the zot exception is being tightened in.
        events: ["push", "manual"]
        allow_pull_requests: true
        allow_forks: true
        partial: false
        secrets:
          - name: "ZOT_TOKEN"
            path: "ci/zot"
            field: "ZOT_TOKEN"
            events: ["push", "manual"]

(The two booleans and partial: false follow the existing rules' precedent — upstream fails closed when Woodpecker omits from_fork, which it does on a same-repo pull request.)

  1. The AppRole policy (docs/woodpecker-ci.md §9k step 1, a root session — auth/ writes are outside the standing token, §4h) gains one block:
# The registry push credential: `annia-ci`, the one writer identity zot has.
# Read-only, and it is the password the publish step passes to `podman push
# --creds` — nothing else in the cluster reads it.
path "secret/data/ci/zot" {
  capabilities = ["read"]
}

§9k's own invariant applies: "no path block without a rule, and no rule without a block." A rule whose path the policy does not name reads nothing, and the extension's failure mode is the one that matters here — it is asked for the secrets of a pipeline it cannot resolve, and the step fails on an empty ZOT_TOKEN rather than on a refused login, three steps away from the cause.

Ordering

The policy write is root-side and must happen before the ConfigMap change is relied on; committing both together is fine because the ConfigMap only takes effect once the AppRole can read the path, and the failure is loud. Per docs/woodpecker-ci.md §9k, the skill's sequence is: write the policy (root) ⇒ commit ⇒ verify. Note that this is not in the secret/annia/* family the VSO resources use — this credential goes to a pipeline step, and a pipeline step can only read a value through from_secret, which is the whole reason the extension exists.

Verification

  • from_secret: ZOT_TOKEN compiles in annia-infra's pipeline (the compile-time gate that does not exist today).
  • A push from the publish step authenticates: podman push --creds "annia-ci:$ZOT_TOKEN" to zot.faun-snapper.ts.net/milner/annia:<tag> succeeds without a prior podman login.
  • Negative, and the point of the scoping: with the AppRole token, reading any path the policy does not name is 403 permission denied — check by names, never by dumping a value.
  • A pull request's run still cannot resolve the name (no rule, and events: ["push", "manual"] is matched exactly).

Follow-on, deliberately not here

Once the credential is in place, milner/**'s anonymous create/update in infrastructure/zot/values.yaml loses its two policies in a change of its own — that is zot/README.md's and values.yaml's stated intent, in that order ("the credential is what has to be in place before the exception can go"), and build-oz/build-router start passing --creds "annia-ci:$ZOT_TOKEN". This issue stops at the credential; retiring the exception is a separate, smaller piece.

Blocking

annia-infra#310's publish step cannot push until this lands — so this is on #310's critical path, alongside milner#107.

## Why `annia-infra#310` ends in a step that pushes `milner/annia:<tag>` to zot, and **that push cannot be authenticated today.** The registry's access control is explicit about it: - `**` — anonymous callers may **read only** (`values.yaml`, `accessControl`), so `annia/**` — where the new image goes — is not anonymously writable; - the only two entries that grant anonymous `create`/`update` are `preview/**` (deliberately permanent) and `milner/**` (deliberately temporary); - `annia-ci` is the one writer identity and can write `**`, and its password is `secret/ci/zot` → `ZOT_TOKEN`. So the credential **exists in Vault and is read by nobody**: the secret extension's AppRole is granted `secret/data/forgejo/token` and `secret/data/irc/herald` and no other path, and it has no rule for `tepichord/annia-infra` on a push event, which means a step asking for `ZOT_TOKEN` would not even compile — `SecretListPipeline` refuses a name no rule serves. `gitops/clusters/milner/infrastructure/zot/README.md` says exactly this: "Nothing reads `secret/ci/zot` yet … that entry is written ahead of the rule that will serve it." This issue is the rule. ## What this changes Two halves, and they are one change because either alone is a no-op: 1. **`apps/woodpecker/secret-extension.yaml`** — a third rule, and a **new rule rather than a wider one on the existing merged-PR rule**, because "a second consumer gets its own rule rather than a wider path, so this file stays a list of what the token is for": the archive-labels action has no business holding the registry password, and the publish step has no business holding the Forgejo token. ```yaml - id: "annia-infra-publish" repo: "tepichord/annia-infra" # A push to main is the only thing that publishes, so nothing on a pull # request can obtain the registry credential — which is the same # direction the zot exception is being tightened in. events: ["push", "manual"] allow_pull_requests: true allow_forks: true partial: false secrets: - name: "ZOT_TOKEN" path: "ci/zot" field: "ZOT_TOKEN" events: ["push", "manual"] ``` (The two booleans and `partial: false` follow the existing rules' precedent — upstream fails closed when Woodpecker omits `from_fork`, which it does on a same-repo pull request.) 2. **The AppRole policy** (`docs/woodpecker-ci.md` §9k step 1, a **root** session — `auth/` writes are outside the standing token, §4h) gains one block: ```hcl # The registry push credential: `annia-ci`, the one writer identity zot has. # Read-only, and it is the password the publish step passes to `podman push # --creds` — nothing else in the cluster reads it. path "secret/data/ci/zot" { capabilities = ["read"] } ``` §9k's own invariant applies: **"no `path` block without a rule, and no rule without a block."** A rule whose path the policy does not name reads nothing, and the extension's failure mode is the one that matters here — it is asked for the secrets of a pipeline it cannot resolve, and the step fails on an **empty** `ZOT_TOKEN` rather than on a refused login, three steps away from the cause. ## Ordering The policy write is root-side and must happen **before** the ConfigMap change is relied on; committing both together is fine because the ConfigMap only takes effect once the AppRole can read the path, and the failure is loud. Per `docs/woodpecker-ci.md` §9k, the skill's sequence is: write the policy (root) ⇒ commit ⇒ verify. Note that this is *not* in the `secret/annia/*` family the VSO resources use — this credential goes to a **pipeline step**, and a pipeline step can only read a value through `from_secret`, which is the whole reason the extension exists. ## Verification - `from_secret: ZOT_TOKEN` **compiles** in `annia-infra`'s pipeline (the compile-time gate that does not exist today). - A push from the publish step authenticates: `podman push --creds "annia-ci:$ZOT_TOKEN"` to `zot.faun-snapper.ts.net/milner/annia:<tag>` succeeds without a prior `podman login`. - **Negative, and the point of the scoping:** with the AppRole token, reading any path the policy does not name is `403 permission denied` — check by names, never by dumping a value. - A pull request's run still cannot resolve the name (no rule, and `events: ["push", "manual"]` is matched exactly). ## Follow-on, deliberately not here Once the credential is in place, `milner/**`'s anonymous `create`/`update` in `infrastructure/zot/values.yaml` **loses its two policies in a change of its own** — that is `zot/README.md`'s and `values.yaml`'s stated intent, in that order ("the credential is what has to be in place before the exception can go"), and `build-oz`/`build-router` start passing `--creds "annia-ci:$ZOT_TOKEN"`. This issue stops at the credential; retiring the exception is a separate, smaller piece. ## Blocking `annia-infra#310`'s publish step cannot push until this lands — so this is on #310's critical path, alongside milner#107.
tepichord added this to the MVP project 2026-09-29 16:30:50 -07:00
Author
Owner

Naina:

Overview

Add the third rule annia-infra-publish to the secret-extension ConfigMap and a read-only secret/data/ci/zot block to the extension's AppRole policy (docs/woodpecker-ci.md §9k step 1, root session). Neither half does anything alone: a rule whose path the policy does not name hands the step an empty ZOT_TOKEN, and a policy path no rule names is unreachable. Sweep the six comments that still say the AppRole reads two paths.

Technical Approach

  1. Add the rule to the rules: list in config.yml — a new rule, never a wider secrets: on annia-infra-merged-pr12:
      - id: "annia-infra-publish"
        repo: "tepichord/annia-infra"
        # A push to main is the only thing that publishes, so nothing on a pull
        # request can obtain the registry credential — the same direction the
        # zot exception is being tightened in.
        events: ["push", "manual"]
        allow_pull_requests: true
        allow_forks: true
        partial: false
        secrets:
          - name: "ZOT_TOKEN"
            path: "ci/zot"
            field: "ZOT_TOKEN"
            events: ["push", "manual"]

Match the file's comment register: why push-only, why the name ZOT_TOKEN, and why path: "ci/zot" is the logical path the extension expands to secret/data/ci/zot (vault.kv.mount: "secret", version: 2)3, 3b. The two booleans are precedent, not trust — upstream fails closed when Woodpecker omits from_fork on a same-repo PR.

  1. Add the policy block to the v policy write woodpecker-secret-extension HCL in §9k step 14:
# The registry push credential: `annia-ci`, the one writer identity zot has.
# Read-only, and it is the password the publish step passes to the registry —
# nothing else in the cluster reads it.
path "secret/data/ci/zot" {
  capabilities = ["read"]
}
  1. Sequence the two halves: root policy write before the ConfigMap is relied on; committing both together is safe5.
# root session (§4h: the standing token cannot write auth/ or policies)
v policy write woodpecker-secret-extension - <<'HCL'
... existing blocks ...
path "secret/data/ci/zot" { capabilities = ["read"] }
HCL
  1. Update the stale comments (exact strings)10, 10b, 10c, 10d, 10e, 10f:
secret-extension.yaml header : "the Forgejo token and the bot's IRC password"  -> three paths
docs/woodpecker-ci.md §9k intro: "the policy below names both paths"            -> three
§9k step 1                     : "Read-only, and only these two paths."          -> three
zot/README.md                  : "Nothing reads `secret/ci/zot` yet ..."        -> served on push/manual to tepichord/annia-infra
zot/README.md + values.yaml ~141: `milner/**` -> `ci/` path now granted, rule for `tepichord/milner` still missing
set-credential.sh header       : "not granted yet ... exactly one path today"   -> undercounts even today (irc/herald is second)
  1. After merge/reconcile: the ConfigMap is read once at start-up, so kubectl -n woodpecker rollout restart deploy/woodpecker-secret-extension (or Flux's checksum roll) before a pipeline can observe the new rule6, 6b. §9k step 5's hand-apply window is the pre-merge test path.

Implementation Details

Files to modify:

File Change
gitops/clusters/milner/apps/woodpecker/secret-extension.yaml third rule + header AppRole comment (two → three paths)
docs/woodpecker-ci.md §9k policy HCL block + intro/step-1 comments
gitops/clusters/milner/infrastructure/zot/README.md "Nothing reads…", milner/** paragraph
gitops/clusters/milner/infrastructure/zot/values.yaml comment lines ~141–149 only
gitops/clusters/milner/infrastructure/zot/set-credential.sh header comment only

Dependencies: none new (extension image 0.2.4 at a568dce; zot chart 0.1.124; Woodpecker chart 3.7.3).

Constraints:

  • config.yml is parsed with KnownFields(true); partial: true is rejected in v0.2.4 (rules[N].partial=true is not supported in v1) — keep partial: false7, 7b.
  • path is logical only; a leading data/ is rejected, so the policy is secret/data/ci/zot while the rule says ci/zot8.
  • ZOT_TOKEN is unique across the three rules, so no allow_override9.
  • Policy contents are parsed per token use → no AppRole re-login needed; only the ConfigMap needs a restart.
  • Verification prints names and HTTP statuses, never values.

Out-of-scope notes:

  • No accessControl change in zot/values.yaml — retiring milner/**'s anonymous create/update is the stated follow-on, and this issue only grants the ci/ path.
  • No --creds added to build-oz/build-router/build-reaper; that follow-on still awaits a rule for tepichord/milner.
  • No change to annia-infra#310 itself. Minor note: podman push --creds "annia-ci:$ZOT_TOKEN" puts the value in argv; the safer form is podman login --password-stdin (zot's own README recipe), and #310 actually uses skopeo copy --dest-creds. Other repo's affair; not expanded here.
  • Optional, not required: a test tying each ConfigMap rule path to a secret/data/… block in §9k's HCL, codifying "no rule without a block" (today doc-only).

Acceptance Criteria

  • config.yml contains rule annia-infra-publish with repo: "tepichord/annia-infra", events: ["push","manual"], and secret ZOT_TOKEN ← path: "ci/zot", field: "ZOT_TOKEN", events: ["push","manual"]; no existing rule's secrets list was widened.
  • §9k step 1's HCL contains exactly path "secret/data/ci/zot" { capabilities = ["read"] }, and the root write is run before the ConfigMap change is relied on.
  • Every stale claim listed in step 4 is updated, and no remaining text says the AppRole names two paths or that the milner/** follow-on is unblocked.
  • python -m pytest tests/ -v is green.
  • Live, post-merge: from_secret: ZOT_TOKEN compiles for a push to main of tepichord/annia-infra, and the AppRole token gets 403 permission denied on a path the policy does not name.

Testing Plan

  • Offline: python -m pytest tests/ -v.
    • test_secret_extension_serves_the_events_woodpecker_checks — new rule has no pull events → skipped, passes.
    • test_secret_extension_serves_the_build_notification_credential — new rule serves ci/zot to annia-infra, not irc/herald → passes.
    • test_the_notification_runs_on_the_events_the_rule_serves — keyed on milner-build-notify → unaffected.
    • test_zot_authorization_keeps_anonymous_reads — unchanged, pins the out-of-scope milner/** policies.
  • Optional new test: parse §9k's v policy write HCL and assert every ConfigMap rule path has a matching secret/data/<path> block.
  • Doc checks: grep -rn "both paths\|only these two paths\|Nothing reads .secret/ci/zot\|exactly one path today" returns no stale hit.
  • Live (after reconcile + rollout restart, or in §9k step 5's hand-apply window):
    • Re-run a push to main in annia-infra; the publish step sees a non-empty ZOT_TOKEN (test -n "$ZOT_TOKEN", never echoing it).
    • Direct probe with the AppRole token: reading secret/data/ci/zot succeeds; reading a path the policy does not name answers 403 permission denied (names/statuses only).
    • A pull-request run still cannot resolve ZOT_TOKEN — no rule serves the name on a pull event, and the rule's events are matched exactly.
  • annia-infra#310 — the publish step this unblocks; the rule/name/event must match its from_secret: ZOT_TOKEN on a push to main.
  • milner#107 — alongside this issue on #310's critical path.
  • Follow-on (separate change): retire milner/**'s anonymous create/update in zot/values.yaml and add --creds to build-oz/build-router/build-reaper.

Research Findings

  • v0.2.4 parses config.yml with KnownFields(true) and rejects partial: true, so partial: false is the only valid—and inert—value; every field in the new rule is valid schema.
  • The rule's path is a logical path under the secret KV v2 mount, expanded to secret/data/ci/zot; that is exactly why the AppRole policy names secret/data/ci/zot, and a leading data/ is rejected.
  • Woodpecker's compiler.Secret.Match collapses only pull events to pull_request; push and manual pass unchanged, and a UI "Restart" of a push pipeline re-fires the same push event and recompiles.
  • allow_pull_requests/allow_forks are consulted only for pull requests, and the rule's events gate is matched first, so the flags cannot widen access for events: ["push","manual"].
  • Vault parses a policy's contents in real time on each token use, so rewriting woodpecker-secret-extension is in force on the existing AppRole token's next call; only the ConfigMap (read once at start-up) needs a rollout restart.
  • zot v2.1.21 selects the longest matching glob, so milner/annia is governed by milner/**, where annia-ci is explicitly named with write actions — the push is authorized either way.
  • podman push --creds is a single-shot auth equal to a prior login but exposes the value in argv; the safer form is podman login --password-stdin, and annia-infra#310's publish step actually uses skopeo copy --dest-creds.

Pre-Mortem

  1. The root-session policy write never actually happened, or happened out of order — the plan changes the HCL only inside docs/woodpecker-ci.md §9k, while the v policy write woodpecker-secret-extension command is an out-of-band runbook action that no check or test enforces. If it is skipped, or run after the ConfigMap is relied on, the new rule resolves against a policy that does not name secret/data/ci/zot and the publish step fails exactly as before, with an empty ZOT_TOKEN three steps from the cause.
  2. The extension keeps serving its old built-in config because no rollout restart followed the merge — config.yml is read once at start-up, so the new rule is inert in the running pods until rollout restart/a Flux checksum roll occurs. The plan names that restart but does not make it a deliverable or add a mechanism that guarantees it, so main can carry the rule while from_secret: ZOT_TOKEN still fails to compile.
  3. Live verification never runs, so a cross-repo mismatch ships undetected — the acceptance criteria that actually exercise the credential depend on annia-infra#310 (still status/planning), a real push to main, and an AppRole token for the negative 403 probe, none of which the implementer may have. The change then merges on the offline suite alone, and any typo in the secret name, path/field, repo identity, or event list surfaces later inside #310 and is misattributed to that workflow rather than to this rule.
Naina: ## Overview Add the third rule `annia-infra-publish` to the secret-extension ConfigMap and a read-only `secret/data/ci/zot` block to the extension's AppRole policy (`docs/woodpecker-ci.md` §9k step 1, root session). Neither half does anything alone: a rule whose path the policy does not name hands the step an empty `ZOT_TOKEN`, and a policy path no rule names is unreachable. Sweep the six comments that still say the AppRole reads two paths. ## Technical Approach 1. **Add the rule to the `rules:` list in `config.yml`** — a new rule, never a wider `secrets:` on `annia-infra-merged-pr`<sup>[1](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/gitops/clusters/milner/apps/woodpecker/secret-extension.yaml?display=source#L74)</sup><sup>[2](https://github.com/lazysteff/woodpecker-vault-secret-extension/blob/a568dced169392cf373cc99daedb421daa5e3cb6/internal/config/config.go#L86-L106)</sup>: ```yaml - id: "annia-infra-publish" repo: "tepichord/annia-infra" # A push to main is the only thing that publishes, so nothing on a pull # request can obtain the registry credential — the same direction the # zot exception is being tightened in. events: ["push", "manual"] allow_pull_requests: true allow_forks: true partial: false secrets: - name: "ZOT_TOKEN" path: "ci/zot" field: "ZOT_TOKEN" events: ["push", "manual"] ``` Match the file's comment register: why push-only, why the name `ZOT_TOKEN`, and why `path: "ci/zot"` is the logical path the extension expands to `secret/data/ci/zot` (`vault.kv.mount: "secret"`, `version: 2`)<sup>[3](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/gitops/clusters/milner/apps/woodpecker/secret-extension.yaml?display=source#L66-L69), [3b](https://github.com/lazysteff/woodpecker-vault-secret-extension/blob/a568dced169392cf373cc99daedb421daa5e3cb6/internal/vault/paths.go#L13-L21)</sup>. The two booleans are precedent, not trust — upstream fails closed when Woodpecker omits `from_fork` on a same-repo PR. 2. **Add the policy block** to the `v policy write woodpecker-secret-extension` HCL in §9k step 1<sup>[4](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/docs/woodpecker-ci.md?display=source#L904-L910)</sup>: ```hcl # The registry push credential: `annia-ci`, the one writer identity zot has. # Read-only, and it is the password the publish step passes to the registry — # nothing else in the cluster reads it. path "secret/data/ci/zot" { capabilities = ["read"] } ``` 3. **Sequence the two halves**: root policy write **before** the ConfigMap is relied on; committing both together is safe<sup>[5](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/docs/woodpecker-ci.md?display=source#L898)</sup>. ```bash # root session (§4h: the standing token cannot write auth/ or policies) v policy write woodpecker-secret-extension - <<'HCL' ... existing blocks ... path "secret/data/ci/zot" { capabilities = ["read"] } HCL ``` 4. **Update the stale comments** (exact strings)<sup>[10](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/gitops/clusters/milner/apps/woodpecker/secret-extension.yaml?display=source#L58), [10b](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/docs/woodpecker-ci.md?display=source#L882), [10c](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/docs/woodpecker-ci.md?display=source#L904), [10d](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/gitops/clusters/milner/infrastructure/zot/README.md?display=source#L297), [10e](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/gitops/clusters/milner/infrastructure/zot/values.yaml?display=source#L141), [10f](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/gitops/clusters/milner/infrastructure/zot/set-credential.sh?display=source#L16)</sup>: ```text secret-extension.yaml header : "the Forgejo token and the bot's IRC password" -> three paths docs/woodpecker-ci.md §9k intro: "the policy below names both paths" -> three §9k step 1 : "Read-only, and only these two paths." -> three zot/README.md : "Nothing reads `secret/ci/zot` yet ..." -> served on push/manual to tepichord/annia-infra zot/README.md + values.yaml ~141: `milner/**` -> `ci/` path now granted, rule for `tepichord/milner` still missing set-credential.sh header : "not granted yet ... exactly one path today" -> undercounts even today (irc/herald is second) ``` 5. **After merge/reconcile**: the ConfigMap is read once at start-up, so `kubectl -n woodpecker rollout restart deploy/woodpecker-secret-extension` (or Flux's checksum roll) before a pipeline can observe the new rule<sup>[6](https://github.com/lazysteff/woodpecker-vault-secret-extension/blob/a568dced169392cf373cc99daedb421daa5e3cb6/cmd/woodpecker-vault-secret-extension/main.go#L29-L57), [6b](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/docs/woodpecker-ci.md?display=source#L1109)</sup>. §9k step 5's hand-apply window is the pre-merge test path. ## Implementation Details Files to modify: | File | Change | |---|---| | `gitops/clusters/milner/apps/woodpecker/secret-extension.yaml` | third rule + header AppRole comment (two → three paths) | | `docs/woodpecker-ci.md` §9k | policy HCL block + intro/step-1 comments | | `gitops/clusters/milner/infrastructure/zot/README.md` | "Nothing reads…", `milner/**` paragraph | | `gitops/clusters/milner/infrastructure/zot/values.yaml` | comment lines ~141–149 only | | `gitops/clusters/milner/infrastructure/zot/set-credential.sh` | header comment only | Dependencies: none new (extension image `0.2.4` at `a568dce`; zot chart `0.1.124`; Woodpecker chart `3.7.3`). Constraints: - `config.yml` is parsed with `KnownFields(true)`; `partial: true` is rejected in v0.2.4 (`rules[N].partial=true is not supported in v1`) — keep `partial: false`<sup>[7](https://github.com/lazysteff/woodpecker-vault-secret-extension/blob/a568dced169392cf373cc99daedb421daa5e3cb6/internal/config/load.go#L30), [7b](https://github.com/lazysteff/woodpecker-vault-secret-extension/blob/a568dced169392cf373cc99daedb421daa5e3cb6/internal/config/validate.go#L162)</sup>. - `path` is logical only; a leading `data/` is rejected, so the policy is `secret/data/ci/zot` while the rule says `ci/zot`<sup>[8](https://github.com/lazysteff/woodpecker-vault-secret-extension/blob/a568dced169392cf373cc99daedb421daa5e3cb6/internal/config/validate.go#L202)</sup>. - `ZOT_TOKEN` is unique across the three rules, so no `allow_override`<sup>[9](https://git.gloom.garden/tepichord/milner/src/commit/d3a5ed016c5ddf8b8319ef82dfbf3e1da2fee038/gitops/clusters/milner/apps/woodpecker/secret-extension.yaml?display=source#L109)</sup>. - Policy *contents* are parsed per token use → no AppRole re-login needed; only the ConfigMap needs a restart. - Verification prints names and HTTP statuses, never values. Out-of-scope notes: - No `accessControl` change in `zot/values.yaml` — retiring `milner/**`'s anonymous `create`/`update` is the stated follow-on, and this issue only grants the `ci/` path. - No `--creds` added to `build-oz`/`build-router`/`build-reaper`; that follow-on still awaits a rule for `tepichord/milner`. - No change to `annia-infra#310` itself. Minor note: `podman push --creds "annia-ci:$ZOT_TOKEN"` puts the value in argv; the safer form is `podman login --password-stdin` (zot's own README recipe), and #310 actually uses `skopeo copy --dest-creds`. Other repo's affair; not expanded here. - Optional, not required: a test tying each ConfigMap rule `path` to a `secret/data/…` block in §9k's HCL, codifying "no rule without a block" (today doc-only). ## Acceptance Criteria - [ ] `config.yml` contains rule `annia-infra-publish` with `repo: "tepichord/annia-infra"`, `events: ["push","manual"]`, and secret `ZOT_TOKEN` ← `path: "ci/zot"`, `field: "ZOT_TOKEN"`, `events: ["push","manual"]`; no existing rule's `secrets` list was widened. - [ ] §9k step 1's HCL contains exactly `path "secret/data/ci/zot" { capabilities = ["read"] }`, and the root write is run before the ConfigMap change is relied on. - [ ] Every stale claim listed in step 4 is updated, and no remaining text says the AppRole names two paths or that the `milner/**` follow-on is unblocked. - [ ] `python -m pytest tests/ -v` is green. - [ ] Live, post-merge: `from_secret: ZOT_TOKEN` compiles for a `push` to `main` of `tepichord/annia-infra`, and the AppRole token gets `403 permission denied` on a path the policy does not name. ## Testing Plan - Offline: `python -m pytest tests/ -v`. - `test_secret_extension_serves_the_events_woodpecker_checks` — new rule has no pull events → skipped, passes. - `test_secret_extension_serves_the_build_notification_credential` — new rule serves `ci/zot` to `annia-infra`, not `irc/herald` → passes. - `test_the_notification_runs_on_the_events_the_rule_serves` — keyed on `milner-build-notify` → unaffected. - `test_zot_authorization_keeps_anonymous_reads` — unchanged, pins the out-of-scope `milner/**` policies. - Optional new test: parse §9k's `v policy write` HCL and assert every ConfigMap rule `path` has a matching `secret/data/<path>` block. - Doc checks: `grep -rn "both paths\|only these two paths\|Nothing reads .secret/ci/zot\|exactly one path today"` returns no stale hit. - Live (after reconcile + rollout restart, or in §9k step 5's hand-apply window): - Re-run a push to `main` in annia-infra; the publish step sees a non-empty `ZOT_TOKEN` (`test -n "$ZOT_TOKEN"`, never echoing it). - Direct probe with the AppRole token: reading `secret/data/ci/zot` succeeds; reading a path the policy does not name answers `403 permission denied` (names/statuses only). - A pull-request run still cannot resolve `ZOT_TOKEN` — no rule serves the name on a pull event, and the rule's `events` are matched exactly. ## Related Issues/PRs - `annia-infra#310` — the publish step this unblocks; the rule/name/event must match its `from_secret: ZOT_TOKEN` on a `push` to `main`. - `milner#107` — alongside this issue on #310's critical path. - Follow-on (separate change): retire `milner/**`'s anonymous `create`/`update` in `zot/values.yaml` and add `--creds` to `build-oz`/`build-router`/`build-reaper`. ## Research Findings - v0.2.4 parses `config.yml` with `KnownFields(true)` and rejects `partial: true`, so `partial: false` is the only valid—and inert—value; every field in the new rule is valid schema. - The rule's `path` is a logical path under the `secret` KV v2 mount, expanded to `secret/data/ci/zot`; that is exactly why the AppRole policy names `secret/data/ci/zot`, and a leading `data/` is rejected. - Woodpecker's `compiler.Secret.Match` collapses only pull events to `pull_request`; `push` and `manual` pass unchanged, and a UI "Restart" of a push pipeline re-fires the same `push` event and recompiles. - `allow_pull_requests`/`allow_forks` are consulted only for pull requests, and the rule's `events` gate is matched first, so the flags cannot widen access for `events: ["push","manual"]`. - Vault parses a policy's contents in real time on each token use, so rewriting `woodpecker-secret-extension` is in force on the existing AppRole token's next call; only the ConfigMap (read once at start-up) needs a rollout restart. - zot v2.1.21 selects the longest matching glob, so `milner/annia` is governed by `milner/**`, where `annia-ci` is explicitly named with write actions — the push is authorized either way. - `podman push --creds` is a single-shot auth equal to a prior login but exposes the value in argv; the safer form is `podman login --password-stdin`, and annia-infra#310's publish step actually uses `skopeo copy --dest-creds`. ## Pre-Mortem 1. **The root-session policy write never actually happened, or happened out of order** — the plan changes the HCL only inside `docs/woodpecker-ci.md` §9k, while the `v policy write woodpecker-secret-extension` command is an out-of-band runbook action that no check or test enforces. If it is skipped, or run after the ConfigMap is relied on, the new rule resolves against a policy that does not name `secret/data/ci/zot` and the publish step fails exactly as before, with an empty `ZOT_TOKEN` three steps from the cause. 2. **The extension keeps serving its old built-in config because no rollout restart followed the merge** — `config.yml` is read once at start-up, so the new rule is inert in the running pods until `rollout restart`/a Flux checksum roll occurs. The plan names that restart but does not make it a deliverable or add a mechanism that guarantees it, so `main` can carry the rule while `from_secret: ZOT_TOKEN` still fails to compile. 3. **Live verification never runs, so a cross-repo mismatch ships undetected** — the acceptance criteria that actually exercise the credential depend on annia-infra#310 (still `status/planning`), a real `push` to `main`, and an AppRole token for the negative `403` probe, none of which the implementer may have. The change then merges on the offline suite alone, and any typo in the secret `name`, `path`/`field`, repo identity, or event list surfaces later inside #310 and is misattributed to that workflow rather than to this rule.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
tepichord/milner#110
No description provided.