Zot push credential for pipelines: secret-extension rule + AppRole path #110
Labels
No labels
blocked
bug
duplicate
effort
high
effort
low
effort
medium
effort
minimal
enhancement
help wanted
invalid
pr
feedback
pr
reviewing
question
status
backlog
status
document
status
done
status
implement
status
pending
status
planning
status
reviewing
waiting
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tepichord/milner#110
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
annia-infra#310ends in a step that pushesmilner/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), soannia/**— where the new image goes — is not anonymously writable;create/updatearepreview/**(deliberately permanent) andmilner/**(deliberately temporary);annia-ciis the one writer identity and can write**, and its password issecret/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/tokenandsecret/data/irc/heraldand no other path, and it has no rule fortepichord/annia-infraon a push event, which means a step asking forZOT_TOKENwould not even compile —SecretListPipelinerefuses a name no rule serves.gitops/clusters/milner/infrastructure/zot/README.mdsays exactly this: "Nothing readssecret/ci/zotyet … 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:
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.(The two booleans and
partial: falsefollow the existing rules' precedent — upstream fails closed when Woodpecker omitsfrom_fork, which it does on a same-repo pull request.)docs/woodpecker-ci.md§9k step 1, a root session —auth/writes are outside the standing token, §4h) gains one block:§9k's own invariant applies: "no
pathblock 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 emptyZOT_TOKENrather 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 thesecret/annia/*family the VSO resources use — this credential goes to a pipeline step, and a pipeline step can only read a value throughfrom_secret, which is the whole reason the extension exists.Verification
from_secret: ZOT_TOKENcompiles inannia-infra's pipeline (the compile-time gate that does not exist today).podman push --creds "annia-ci:$ZOT_TOKEN"tozot.faun-snapper.ts.net/milner/annia:<tag>succeeds without a priorpodman login.403 permission denied— check by names, never by dumping a value.events: ["push", "manual"]is matched exactly).Follow-on, deliberately not here
Once the credential is in place,
milner/**'s anonymouscreate/updateininfrastructure/zot/values.yamlloses its two policies in a change of its own — that iszot/README.md's andvalues.yaml's stated intent, in that order ("the credential is what has to be in place before the exception can go"), andbuild-oz/build-routerstart 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.Naina:
Overview
Add the third rule
annia-infra-publishto the secret-extension ConfigMap and a read-onlysecret/data/ci/zotblock 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 emptyZOT_TOKEN, and a policy path no rule names is unreachable. Sweep the six comments that still say the AppRole reads two paths.Technical Approach
rules:list inconfig.yml— a new rule, never a widersecrets:onannia-infra-merged-pr12:Match the file's comment register: why push-only, why the name
ZOT_TOKEN, and whypath: "ci/zot"is the logical path the extension expands tosecret/data/ci/zot(vault.kv.mount: "secret",version: 2)3, 3b. The two booleans are precedent, not trust — upstream fails closed when Woodpecker omitsfrom_forkon a same-repo PR.v policy write woodpecker-secret-extensionHCL in §9k step 14: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:
gitops/clusters/milner/apps/woodpecker/secret-extension.yamldocs/woodpecker-ci.md§9kgitops/clusters/milner/infrastructure/zot/README.mdmilner/**paragraphgitops/clusters/milner/infrastructure/zot/values.yamlgitops/clusters/milner/infrastructure/zot/set-credential.shDependencies: none new (extension image
0.2.4ata568dce; zot chart0.1.124; Woodpecker chart3.7.3).Constraints:
config.ymlis parsed withKnownFields(true);partial: trueis rejected in v0.2.4 (rules[N].partial=true is not supported in v1) — keeppartial: false7, 7b.pathis logical only; a leadingdata/is rejected, so the policy issecret/data/ci/zotwhile the rule saysci/zot8.ZOT_TOKENis unique across the three rules, so noallow_override9.Out-of-scope notes:
accessControlchange inzot/values.yaml— retiringmilner/**'s anonymouscreate/updateis the stated follow-on, and this issue only grants theci/path.--credsadded tobuild-oz/build-router/build-reaper; that follow-on still awaits a rule fortepichord/milner.annia-infra#310itself. Minor note:podman push --creds "annia-ci:$ZOT_TOKEN"puts the value in argv; the safer form ispodman login --password-stdin(zot's own README recipe), and #310 actually usesskopeo copy --dest-creds. Other repo's affair; not expanded here.pathto asecret/data/…block in §9k's HCL, codifying "no rule without a block" (today doc-only).Acceptance Criteria
config.ymlcontains ruleannia-infra-publishwithrepo: "tepichord/annia-infra",events: ["push","manual"], and secretZOT_TOKEN←path: "ci/zot",field: "ZOT_TOKEN",events: ["push","manual"]; no existing rule'ssecretslist was widened.path "secret/data/ci/zot" { capabilities = ["read"] }, and the root write is run before the ConfigMap change is relied on.milner/**follow-on is unblocked.python -m pytest tests/ -vis green.from_secret: ZOT_TOKENcompiles for apushtomainoftepichord/annia-infra, and the AppRole token gets403 permission deniedon a path the policy does not name.Testing Plan
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 servesci/zottoannia-infra, notirc/herald→ passes.test_the_notification_runs_on_the_events_the_rule_serves— keyed onmilner-build-notify→ unaffected.test_zot_authorization_keeps_anonymous_reads— unchanged, pins the out-of-scopemilner/**policies.v policy writeHCL and assert every ConfigMap rulepathhas a matchingsecret/data/<path>block.grep -rn "both paths\|only these two paths\|Nothing reads .secret/ci/zot\|exactly one path today"returns no stale hit.mainin annia-infra; the publish step sees a non-emptyZOT_TOKEN(test -n "$ZOT_TOKEN", never echoing it).secret/data/ci/zotsucceeds; reading a path the policy does not name answers403 permission denied(names/statuses only).ZOT_TOKEN— no rule serves the name on a pull event, and the rule'seventsare matched exactly.Related Issues/PRs
annia-infra#310— the publish step this unblocks; the rule/name/event must match itsfrom_secret: ZOT_TOKENon apushtomain.milner#107— alongside this issue on #310's critical path.milner/**'s anonymouscreate/updateinzot/values.yamland add--credstobuild-oz/build-router/build-reaper.Research Findings
config.ymlwithKnownFields(true)and rejectspartial: true, sopartial: falseis the only valid—and inert—value; every field in the new rule is valid schema.pathis a logical path under thesecretKV v2 mount, expanded tosecret/data/ci/zot; that is exactly why the AppRole policy namessecret/data/ci/zot, and a leadingdata/is rejected.compiler.Secret.Matchcollapses only pull events topull_request;pushandmanualpass unchanged, and a UI "Restart" of a push pipeline re-fires the samepushevent and recompiles.allow_pull_requests/allow_forksare consulted only for pull requests, and the rule'seventsgate is matched first, so the flags cannot widen access forevents: ["push","manual"].woodpecker-secret-extensionis in force on the existing AppRole token's next call; only the ConfigMap (read once at start-up) needs a rollout restart.milner/anniais governed bymilner/**, whereannia-ciis explicitly named with write actions — the push is authorized either way.podman push --credsis a single-shot auth equal to a prior login but exposes the value in argv; the safer form ispodman login --password-stdin, and annia-infra#310's publish step actually usesskopeo copy --dest-creds.Pre-Mortem
docs/woodpecker-ci.md§9k, while thev policy write woodpecker-secret-extensioncommand 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 namesecret/data/ci/zotand the publish step fails exactly as before, with an emptyZOT_TOKENthree steps from the cause.config.ymlis read once at start-up, so the new rule is inert in the running pods untilrollout 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, somaincan carry the rule whilefrom_secret: ZOT_TOKENstill fails to compile.status/planning), a realpushtomain, and an AppRole token for the negative403probe, none of which the implementer may have. The change then merges on the offline suite alone, and any typo in the secretname,path/field, repo identity, or event list surfaces later inside #310 and is misattributed to that workflow rather than to this rule.