fix(security): enforce cosign verify on the live root OCIRepository - #3001
Conversation
flux-operator owns ocirepository/flux-system and KSail writes no spec key on it during a routine deploy, so spec.workload.flux.verify covered bootstrap only and the live root source pulled unverified. Have the operator write the field via spec.kustomize.patches, and extend validate-flux-verify to assert the patch exists and targets the root source -- a mistargeted patch renders nothing, exits 0 and warns nobody.
…t signers The policy is written twice -- bootstrap in the cluster config, running cluster in the FluxInstance -- because neither covers the other's window. Nothing compared them, so tightening one would silently leave the other trusting a publisher it should not. Also make the mistargeted-patch report independent of patch order, and name only the differing signers instead of dumping both lists.
✅
|
CI is green at the current head and this draft carries no qualifying review, so requesting the @coderabbitai review |
|
✅ Action performedReview finished.
|
📝 WalkthroughWalkthroughThe PR adds Cosign verification to the Flux root Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/validate-flux-verify/instance.go`:
- Around line 309-321: Update findInstance to select only the FluxInstance whose
metadata.name is "flux" and metadata.namespace is "flux-system"; ignore other
resource kinds and identities, and return an error when the expected resource is
missing or appears more than once instead of accepting the first match.
- Around line 104-122: Update verifyWrite and the matching-patch processing to
evaluate every add, replace, and remove operation in declared order, retaining
the final effective /spec/verify state rather than the first value. Ensure
validation rejects an absent final value or a final remove, and add regression
coverage for add-then-remove operations within one patch and across separate
patches.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9c102261-1894-43fa-89b7-9c8611523a42
📒 Files selected for processing (7)
.github/workflows/ci.yamlk8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yamlksail.prod.yamlscripts/validate-flux-verify/instance.goscripts/validate-flux-verify/instance_test.goscripts/validate-flux-verify/main.goscripts/validate-flux-verify/main_test.go
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
k8s/**/*.yaml
📄 CodeRabbit inference engine (AGENTS.md)
k8s/**/*.yaml: Kubernetes base files underk8s/bases/are immutable; modify them through Kustomizepatches:in provider or cluster overlays.
Preserve Flux dependency order:bootstrap→infrastructure-controllers→infrastructure→apps.
Use one Kubernetes resource per file, except explicitly whitelisted vendored CDI and KubeVirt operator bundles.
Use kebab-case directories and Kind-led filenames in component folders; qualify filenames with a purpose when multiple resources share a Kind.
Before a manifest PR, run schema-aware KSail validation when available, otherwise build bothk8s/clusters/local/andk8s/clusters/prod/withkubectl kustomize; use client-side dry-run for changed individual manifests.
Files:
k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml
**/*.{yaml,yml}
📄 CodeRabbit inference engine (AGENTS.md)
Use Conventional Commit messages and create pull requests as drafts; keep changes small and focused.
Files:
k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yamlksail.prod.yaml
ksail*.yaml
📄 CodeRabbit inference engine (AGENTS.md)
Validate KSail configuration and manifests statically; do not start a cluster or mutate
~/.kube/configfor maintenance work.
Files:
ksail.prod.yaml
.github/workflows/*.yaml
📄 CodeRabbit inference engine (AGENTS.md)
CI validation for Kubernetes-related changes must remain static and must not depend on a running cluster;
flux checkand other cluster-dependent checks are not maintenance validation.
Files:
.github/workflows/ci.yaml
🧠 Learnings (4)
📚 Learning: 2026-08-04T13:06:25.700Z
Learnt from: devantler
Repo: devantler-tech/platform PR: 2944
File: scripts/validate-flux-verify/main_test.go:300-305
Timestamp: 2026-08-04T13:06:25.700Z
Learning: For validator acceptance tests under scripts/**/main_test.go, fixed repository-relative paths passed to os.ReadFile do not require //nolint:gosec: golangci-lint does not run gosec on these test-file calls. Apply gosec G304 suppressions only to non-test Go code. Use scripts/validate-dr-signing/main_test.go as the reference analogue.
Applied to files:
scripts/validate-flux-verify/main_test.go
📚 Learning: 2026-07-01T21:13:36.950Z
Learnt from: devantler
Repo: devantler-tech/platform PR: 2359
File: k8s/bases/apps/actual-budget/helm-release.yaml:62-111
Timestamp: 2026-07-01T21:13:36.950Z
Learning: When reviewing Kustomize/Helm YAML in this repo, keep the base vs provider overlay split: `k8s/bases/apps/**` and `k8s/bases/infrastructure/**` should contain each app’s full, environment-agnostic configuration (including base-level postRenderer Kustomize patches such as deployment strategy, topology spread, probes, and env injection). `k8s/providers/{docker,hetzner}/**` should only add small provider-specific deltas (e.g., `interval`, `persistence.size`) via patch files (like `k8s/providers/<provider>/apps/<app>/patches/helm-release-patch.yaml`). If configuration is identical across providers (e.g., OIDC/OAuth env vars where `${domain}` is resolved per cluster via envsubst), it belongs in the base and must not be duplicated into provider overlays.
Applied to files:
k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml
📚 Learning: 2026-07-28T00:33:10.828Z
Learnt from: devantler
Repo: devantler-tech/platform PR: 2825
File: .github/workflows/dr-rebuild.yaml:70-73
Timestamp: 2026-07-28T00:33:10.828Z
Learning: For GitHub Actions concurrency configuration in this repo, don’t use `queue: single` on a shared production concurrency group (e.g., `prod-deploy`) that is used for normal production deployments—`queue: single` will cancel/replace any pending run in that group. Normal production deployments must use a uniform `queue: max` for that shared group to ensure serialization without coalescing. If you need DR rebuild coalescing (replacement of pending DR rebuild runs), it must use a separate DR-only concurrency scope/group (not the shared production `prod-deploy` group).
Applied to files:
.github/workflows/ci.yaml
📚 Learning: 2026-07-29T07:29:34.804Z
Learnt from: devantler
Repo: devantler-tech/platform PR: 2848
File: scripts/validate-publication-order/main.go:315-353
Timestamp: 2026-07-29T07:29:34.804Z
Learning: When reviewing GitHub Actions workflow steps with `run:` blocks, remember that the default non-interactive Bash invocation won’t expand shell aliases unless the script explicitly enables it (e.g., `shopt -s expand_aliases`). Therefore, when assessing potential command-name shadowing (e.g., whether an alias could change what command runs), only treat alias-based behavior as possible if the step explicitly opts into alias expansion via `shopt -s expand_aliases` (or equivalent).
Applied to files:
.github/workflows/ci.yaml
…nce identity Two fail-open holes found in review, both of which let an unverified cluster pass the gate: A patch could add /spec/verify and a later operation remove it again -- the first write was taken as the answer. The state is now folded over every operation in declared order, so only the effective value counts. The instance was selected by kind alone, so a second FluxInstance carrying a correct patch satisfied the check while the deployed one carried none. It is now selected by identity, and a duplicate is refused rather than guessed.
Both findings from the previous review are fixed and their threads resolved; CI is green at the new head, which stales the earlier review. Re-requesting at this head. @coderabbitai review |
|
|
CodeRabbit is rate limited at this head ( @codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Codex reports its code-review usage limit is reached (14:12:32Z) — a service failure, not a finding — so advancing to the last lane in priority order. |
|
@cursor review |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_0f050e59-f980-4482-9963-bb16db4a9d55) |
devantler
left a comment
There was a problem hiding this comment.
🤖 Generated by the Agentic Engineer
Self-review (fallback — CodeRabbit, Codex and Cursor Bugbot unavailable)
Reviewed commit: 63c61e5a251ef35a83ab21e98ec6b4aaecddf26a
Per-lane evidence at this exact head, checked directly on this PR:
- CodeRabbit —
CodeRabbitcommit statussuccess — Review rate limitedat 2026-08-06T14:01:27Z. No review object exists at this head; the twocoderabbitai[bot]entries here are zero-length reply containers from the resolved threads, not reviews. (Its review of the previous head5d3d6621was real —**Actionable comments posted: 2**— and both findings are fixed and resolved below.) - Codex — issue comment at 2026-08-06T14:12:32Z: usage limits for code reviews reached. Service failure, no findings.
- Cursor Bugbot — check-run
conclusion: neutralwithoutput.title: Error, summaryBugbot run failed(9s), plus acursor[bot]comment at 14:22:18Z stating the usage limit was reached. Zero inline comments, no review object — the run never started. No retry window is stated, so the lane is not merely throttled.
All three are quota/service states rather than verdicts on this change, so the local round applies.
Scope reviewed
The full diff at this head: the FluxInstance kustomize patch, the ksail.prod.yaml comment correction, the CI wiring, and scripts/validate-flux-verify (instance.go, instance_test.go, main.go, main_test.go).
Evidence, not assertion
- RED before GREEN.
TestRealFluxInstanceValidatesfailed on the shipped manifest with exactly the #2922 defect before the patch was added, and passes after. - The new guard is ablated, not assumed. Repointing the patch target is a 2-line change; the guard fires with the specific message and the post-revert control is clean. Separately,
kubectl kustomizerenders the mistargeted patch at exit 0 with no warning — which is the whole reason the guard exists. - Both review fixes are ablated too. Reverting the operation fold to first-write-wins fails exactly the two add-then-remove arms; reverting instance selection to kind-only fails exactly the three identity arms. Neither test set is vacuous.
- The drift check is ablated. Changing one signer subject in
ksail.prod.yaml(2 lines) fails the build naming only the differing entry; post-revert clean. - Live baseline recorded for the post-merge check (read-only,
admin@prod):ocirepository/flux-systemspec.verifyABSENT, generation 3,Ready=True;fluxinstance/fluxcurrently carries 6 patches. After the next prod deploy the expectation isspec.verifypresent and 7 patches. - Full static validation green:
ksail --config ksail.prod.yaml workload validate(564 files), both overlay builds,validate-naming.py,go vet,gofmt, and all 31 CI checks at this head withmergeStateStatus: CLEAN.
Findings
No P0/P1.
Two behaviours are deliberate rather than overlooked, recorded here so they are not mistaken for oversights:
- nit — the drift check is order-sensitive.
reflect.DeepEqualovermatchOIDCIdentitymeans reordering the three matchers in one file without the other fails the build, though Flux accepts a match from any entry so the reorder is semantically neutral. Kept: the failure direction is closed, the message says exactly what differs, and "make the two blocks identical" is the intended contract — a comparator that tolerated reordering would be the first step toward tolerating difference. - nit —
targetsRootSourceaccepts an absentnamespaceon the target. A kustomize target without a namespace matches any, which still includes the root source, so treating it as a hit is correct; only a namespace that is present and different is a miss. An absent target name, by contrast, is refused, because that genuinely leaves the subject unstated.
Verdict
Verdict: no P0/P1 findings
Readiness — all three conditions met at
|
Evicted from the merge queue — root-caused, and NOT re-queuedThis PR was enqueued and then evicted when its Both A failure that reproduces while deploying The real chain is the 11-day gated Cilium rollout: Per the merge-queue rule in Publish was skipped in the failed run, so no speculative artifact reached the mutable tag and prod The PR itself is unchanged and still pentad-clear at |
This PR's premise changed today —
|
| Manager | Operation | Time | Owns |
|---|---|---|---|
ksail |
Update | 2026-08-06T14:28:48Z | (no spec keys) |
flux-operator |
Apply | 2026-08-06T14:28:53Z | interval, provider, ref, secretRef, url, verify |
So verify is owned by flux-operator — the manager this PR argues it must be owned by — and it
landed five seconds after a ksail write, today. The bootstrap/update path evidently ran against
prod at 14:28Z and propagated spec.workload.flux.verify from ksail.prod.yaml.
Two things follow, and I am deliberately not resolving them inside this PR:
- The mechanism this PR advocates is confirmed correct — the operator, not a hand-written
override, is the right writer, and it is demonstrably capable of owning the field. - The stated defect is currently not reproducible. Shipping a fix whose RED condition no longer
holds would be shipping on a stale premise.
What is still worth having
The control being present today is not the same as it being enforced. This PR's other half —
scripts/validate-flux-verify extended to assert the flux-instance.yaml half on every pull
request — is what stops the field regressing silently, and that value is unchanged. ksail's write
is a bootstrap-path effect; nothing in CI asserts it survives.
Also worth flagging
This 14:28Z ksail write sits oddly against #2963, which measures that the Cilium rollout gate has
been skipping the pipeline's only ksail cluster update since 2026-07-26. Either that step ran by
another route today, or the gate's suppression is narrower than #2963 records. That is a question for
#2963, and I have posted today's widened Cilium evidence there separately.
Not promoting or merging this while its premise reads stale. Next step is to re-derive the RED
condition — is there still a path on which the root source ends up unverified? — and either rescope
this to the CI-enforcement half alone, or close it and keep #2922 open against the real gap.
Correction to my previous comment —
|
🛑 STOP — this configuration is live on prod right now and Flux is REFUSING it. Do not merge as written.Following up on my correction above, I read the root source's conditions rather than just its
So the central mechanism of this PR does not work. The RED I built it against was real, the delivery Live impact, and why it cannot self-healThe root It is also self-locking. The offending This is the failure mode filed as #3005, now with a concrete instance: a never-merged candidate did What this PR needs before it goes anywhere near the queue again
Given (1) is unresolved and the deploy path is separately blocked by #2963, I am leaving this PR Correcting myself explicitly: my previous comment said "do not close this PR on the grounds that the |
Root cause is upstream, and it is a docs-vs-implementation gap — not our misconfigurationI said above I would not guess at the remedy. Here is the evidenced version. Flux documents So the three-entry list in this PR is exactly what the documented API describes, which is why it The underlying library cannot honour it. The error text is not Flux's — it comes from The only shape that can express our allow-listBecause both fields are Go regexps and exactly one identity is permitted, an allow-list has to be That is a candidate, still unproven against a real verifier. It is now grounded in documented Two follow-ups this creates
Sources: Flux OCIRepository API docs · |
Converted back to draft — safety only. I am not taking this PR's fix; the analysis above owns it. This was sitting The readiness condition that no longer holds is the third one: tried and evaluated as a user. The I have deliberately not pushed the regex-alternation fix — a second instance editing this branch One datum for that verification, measured independently at ~19:2xZ: the live remedy for the cluster |
…pports cosign's keyless verification rejects a multi-entry matchOIDCIdentity outright and fails closed for the whole set, so the three-entry matcher verified nothing while reading as stricter than a single one. Both halves now alternate the same three signers inside one subject regex, and the validator fails the build on any list longer than one so the shape cannot return.
Validation for
|
| input | result |
|---|---|
| the real SAN above | match |
| the real issuer above | match |
ci.yaml@refs/heads/main (near miss, never trusted) |
no match |
Plus 7 further negative controls: wrong org, unlisted workflow, feature branch, branch-prefix
extension (mainX), empty merge-queue suffix, and an unanchored prefix. All reject. The pattern is
exactly the union of the three previous subjects — no signer gained or lost.
3. RED/GREEN on the guard
Ran the validator against the exact config that halted prod, and against the fix:
| input | result |
|---|---|
| 3-entry matcher (what shipped) | exit 1 — "has 3 entries, and cosign supports exactly ONE" |
| 1-entry alternation (this PR) | exit 0 |
The guard is asserted in both halves independently, not just the cluster-config one. That matters
because the existing drift check only makes the two copies equal — two identically broken copies
satisfy it. Ablation: with the cluster-config half fixed and the patch half left broken, the failure
message changes from the drift error to the arity error, proving the patch-half check is genuinely
reached rather than shadowed.
4. One existing test was inverted, deliberately
one complete matcher among incomplete ones validates asserted that a 2-entry list should pass,
reasoning that one usable entry is enough. That describes Flux's documented OR semantics, which
cosign does not implement. The arm is now a second matcher entry is rejected even when one entry is complete, with the reasoning recorded inline. A non-vacuity control was added alongside it — the
same three signers alternated in one subject must still validate — so a guard that rejected
everything could not pass.
go test ./scripts/validate-flux-verify/... green (incl. TestRealConfigValidates,
TestRealFluxInstanceValidates, TestRealHalvesAgree, which exercise the real repo files);
go vet clean.
Still outstanding, and not in this PR
Efficacy is proven for the artifact live right now, by hand. Nothing yet proves it on every future
deploy, which is the general form of this defect — I have filed that separately rather than widening
this PR, alongside #3006 (a bad FluxInstance change cannot be corrected through GitOps).
@coderabbitai review
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Changed files ==='
git diff --name-status HEAD^ HEAD
printf '%s\n' '=== Go package outline ==='
ast-grep outline scripts/validate-flux-verify --items all --type function
printf '%s\n' '=== Matcher and verification references ==='
rg -n -C 4 'matchOIDCIdentity|verify|checkNoDrift|validateInstance|configVerifyBlock' \
scripts/validate-flux-verify \
ksail.prod.yaml \
k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml \
.github/workflows/ci.yaml
printf '%s\n' '=== Current patch diff ==='
git diff --unified=80 HEAD^ HEAD -- \
.github/workflows/ci.yaml \
ksail.prod.yaml \
k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml \
scripts/validate-flux-verifyLength of output: 145290 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Current effective matcher lines ==='
nl -ba ksail.prod.yaml | sed -n '228,256p'
nl -ba k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml | sed -n '194,218p'
printf '%s\n' '=== Validator matcher checks ==='
nl -ba scripts/validate-flux-verify/main.go | sed -n '108,168p;360,402p'
nl -ba scripts/validate-flux-verify/instance.go | sed -n '176,232p'
printf '%s\n' '=== Tests that compile or match issuer and subject regexes ==='
rg -n -C 5 'regexp\.|Compile|MatchString|token\.actions|githubusercontent|actions\.githubusercontent' \
scripts/validate-flux-verify
printf '%s\n' '=== Parent configuration issuer values ==='
git show HEAD^:ksail.prod.yaml | sed -n '/matchOIDCIdentity:/,+8p'
git show HEAD^:k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml | sed -n '/matchOIDCIdentity:/,+14p'Length of output: 1291 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Current effective matcher lines ==='
sed -n '228,256p' ksail.prod.yaml
sed -n '194,218p' k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml
printf '%s\n' '=== Validator matcher checks ==='
sed -n '108,168p;360,402p' scripts/validate-flux-verify/main.go
sed -n '176,232p' scripts/validate-flux-verify/instance.go
printf '%s\n' '=== Tests that compile or match issuer and subject regexes ==='
rg -n -C 5 'regexp\.|Compile|MatchString|token\.actions|githubusercontent|actions\.githubusercontent' \
scripts/validate-flux-verify || true
printf '%s\n' '=== Parent configuration issuer values ==='
git show HEAD^:ksail.prod.yaml | sed -n '/matchOIDCIdentity:/,+8p'
git show HEAD^:k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml | sed -n '/matchOIDCIdentity:/,+14p'Length of output: 20334
✏️ Learnings added
|
There was a problem hiding this comment.
🤖 Generated by the Agentic Engineer
Self-review (fallback — CodeRabbit, Codex and Cursor Bugbot unavailable)
Reviewed commit: db0b8ca7c8af0f26d8acda4556e8769a74b6baa3
Per-lane evidence, checked directly on this PR at this head rather than taken from a portfolio digest:
- CodeRabbit — commit status at head is
success / "Review rate limited". That is the provider's own quota signal, not a verdict on this change; the lane will not deliver here. - Codex — zero review objects at this head; its only comment on this PR (14:12:32Z) reads "You have reached your Codex usage limits for code reviews."
- Cursor Bugbot — no check-run at this head. The lane is usage-limited account-wide (measured across 35 PRs today, this one included); re-requesting cannot succeed and only burns comments, so it was not re-triggered.
What I checked, and how
Correctness of the matcher was established positively, which is the whole point — the defect this PR fixes was a control that passed every structural check while verifying nothing.
- Who really signed the live artifact. No
packages: readon this host, socosign verifyagainst GHCR returnsUNAUTHORIZED. Resolved instead through the public Sigstore transparency log, which needs no registry credential: all 3 Rekor DSSE entries forlatest@sha256:e6039d0a…carry SAN…/workflows/ci.yaml@refs/heads/gh-readonly-queue/main/pr-3001-72fe7919…and issuerhttps://token.actions.githubusercontent.com. - The new matcher accepts exactly that, tested with Go's
regexp(cosign's engine): real subject → match, real issuer → match,ci.yaml@refs/heads/main→ no match. Plus 7 further negative controls (wrong org, unlisted workflow, feature branch,mainXprefix extension, empty queue suffix, unanchored prefix). The pattern is the exact union of the three previous subjects — no signer gained or lost. - RED/GREEN on the guard, against the literal config that halted prod: 3-entry → exit 1 naming the fix; 1-entry alternation → exit 0. Asserted independently in both halves, with an ablation proving the FluxInstance-half check is genuinely reached and not shadowed by the drift check (drift only proves the two copies are equal, and two identically broken copies satisfy it).
- Suite green —
go test ./scripts/validate-flux-verify/..., includingTestRealConfigValidates,TestRealFluxInstanceValidatesandTestRealHalvesAgree, which exercise the real repo files.go vetclean. - Second half is a real fix, not collateral.
ksail.prod.yamlonmaincarries the same broken 3-entry shape today, so a disaster-recovery rebuild would currently fail exactly as prod did — with no cluster left to recover from.
One judgement call, disclosed rather than hidden
Three comments added here narrate history (the inverted test arm, and the two "this halted prod" rationales). That runs against the repo's own describe-the-as-is rule, and I kept them deliberately: the inverted expectation looks wrong without its reason and would otherwise be "corrected" back, and the guard's cost/benefit is unreadable without knowing what it caught. I would accept a reviewer trimming them; I would not accept trimming them to the point where the next reader reinstates the multi-entry form.
Verdict
No P0/P1 findings.
CI workflow was never created for db0b8ca7 (the sole run is a queued GitHub-managed dynamic one), because GitHub Actions is in a critical outage. This PR must stay a draft until CI actually runs green: merging enqueues a merge_group run that deploys to the real prod cluster, and that is not something to do on unverified checks — least of all on this PR.

Why
The platform's root artifact — the one every controller, tenant binding and policy arrives through —
is pulled from a mutable tag with nothing checking who produced it. This turns signature verification
on for the running cluster.
An earlier version of this PR did that with a three-entry signer list, which cosign rejects
outright: it fails closed for the whole set, so the control verified nothing and halted all GitOps
delivery on prod for about five and a half hours (#3005). That is fixed here, and the fix is proven
against the real artifact rather than reasoned about.
What
publishers alternated inside a single pattern. No signer is added or removed.
identical broken shape on
maintoday — so a DR rebuild would currently fail the same way, at theworst possible moment.
shape cannot come back silently.
Merge safety
The blocker that made this PR dangerous is resolved and independently verified: the artifact
currently live on
latestis confirmed — from the public Sigstore transparency log, needing noregistry access — to have been signed by a publisher this matcher accepts. Evidence is in a comment
below rather than here.
Fixes #2922
Part of #3005