Repository navigation
Make root OCI publication verified and atomic before moving latest #2627
Description
🤖 Generated by the Codex Daily AI Engineer instance.
Problem
Production's root Flux OCIRepository tracks ghcr.io/devantler-tech/platform/manifests:latest every minute, but live spec.verify is null. The intended keyless Cosign policy is nested at spec.cluster.verify in ksail.prod.yaml; KSail v7.167 reads spec.workload.flux.verify, so the unknown field is silently ignored.
The production deploy currently moves mutable latest before signing and attesting it. With verification disabled, Flux can fetch an unsigned artifact immediately after push. Even after the verify config is corrected, Flux can fetch after signing but before provenance/SBOM attestations complete. The DR rebuild is coupled to the same defect: it pushes an unsigned artifact, lacks id-token: write, and its workflow identity is not in the intended allow-list, so simply moving verify would make a clean DR reconcile fail.
Live evidence on 2026-07-13: root source ref.tag=latest, interval=1m0s, spec.verify=null, Ready on the current main artifact. This is pre-existing and separate from the GHCR credential propagation fix in #2625.
Proposed direction
- Move the Cosign configuration to the KSail-supported
spec.workload.flux.verifypath and add a rendered/live-shape regression that fails if root verification is absent. - Make publication atomic from Flux's perspective: publish to an immutable staging reference, sign and attest it, then promote/repoint
latest; or suspend/repoint the root source with explicit failure recovery until the complete supply-chain bundle exists. - Give DR a valid signed-artifact path (
id-token: writeplus a narrowly allowed DR identity), or have DR consume an already signed immutable artifact instead of publishing unsigned bytes. - Prove normal merge-group, manual CD, failed delivery, and from-zero DR behavior.
Acceptance criteria
- The live root OCIRepository has non-null Cosign verification with only the intended production identities.
- Flux cannot observe a new artifact until signature and required attestations are complete.
- Any failed push/sign/attest leaves the last verified artifact active.
- DR can rebuild from zero without weakening verification and finishes on a signed, allowed artifact.
- Static tests validate config nesting, workflow permissions/identity, and publication ordering.
Rough size: medium; coordinate the KSail config correction, publication transaction, and DR signer path in one rollout so no intermediate state breaks production or recovery.
Metadata
Metadata
Assignees
Labels
Type
Fields
Projects
- StatusShow more project fields✅ Done
Activity
coderabbitai commentedon Jul 13, 2026
🔗 Related PRs
#2317 - ci: migrate deploy-prod Flux diagnostics to shared diagnose-flux composite [merged]
#2319 - feat(security): cosign-verify the infrastructure Flux OCI artifact [merged]
#2399 - feat: accept devantler-tech/actions cosign subjects during the publish-workflow repoint [merged]
#2404 - fix: tenant artifact verification rejects the dual signing identities from
#2399[merged]#2457 - fix(talos): accept the actions publish-app identity at node image verification [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
feat(security): sign the DR-published root artifact and allow its ide…
devantler commentedon Jul 28, 2026
Premise re-verified (2026-07-28), and it is sharper than filed
This issue was written 2026-07-13 against KSail v7.167; the tap is now v7.177.3, so I re-checked rather than inheriting it. All of it still holds, and the live contrast makes the severity clearer than the original text:
spec.verifyflux-system/flux-system(root — delivers the whole platform)ascoachingogvaner,aws,doggy-countdown,github-config,wedding-appcosignEvery tenant artifact is signature-verified; the infrastructure root is not. The posture is inverted exactly where blast radius is largest.
Confirmations:
yq '.spec.cluster.verify' ksail.prod.yamlresolves — the block sits atspec.cluster.verify. KSail readsWorkload.Flux.Verify(pkg/apis/cluster/v1alpha1/types.go, pinned by that package'senvvar_drift_test.goasWorkload.Flux.Verify.*). So the config is inert, silently.deploy-prod/action.ymlthe order isksail workload push→ install cosign → sign → attest, solatestmoves before a signature exists. Flux polls the root atinterval: 1m0s.dr-rebuild.yamljobrebuilddeclares onlycontents: read+packages: write; the file contains nocosigninvocation.The ordering is load-bearing — this is why the issue has not moved
The four parts are not independently shippable, and the dependency runs opposite to the order they are listed in. Moving
verifyto the path KSail reads is the LAST step, not the first — doing it first switches verification on while two writers still publish unsignable or unsigned artifacts, converting a latent gap into a production and recovery outage.Sequencing:
latestmoves, so Flux cannot observe an unsigned artifact in the push→sign window.spec.verifygates on signatures only, so this is a criteria question, not a gate question.spec.workload.flux.verifyand add the rendered/live-shape regression. Only safe once 1–3 hold.Step 1 is shipped as #2839 / PR #2840 (verification deliberately left off). Steps 2–4 remain here; I have not filed them as children yet, since 2 and 3 want their own measurement of the push→sign window before being specified.
feat(security): give the DR rebuild a signed-artifact publication path (
test(ci): pin the prod publication ordering so evidence precedes release
32 remaining items
devantler commentedon Aug 5, 2026
Revalidation against current evidence (2026-08-05) — scope has moved, issue still valid
Picked this up as the oldest open
Securityissue and re-checked its premises against live prodrather than implementing the 2026-07-13 plan as written. Two of the four proposed directions have
changed state.
Direction 1 (move Cosign config to
spec.workload.flux.verify) — DONE. #2919 landed it, andplatform CI now pins
KSAIL_VERSION 7.178.14, which post-dates ksail#6454's update-pathre-assertion.
Acceptance criterion 1 (live root OCIRepository has non-null verification) — STILL FAILING, and
that half is now tracked in sharper form by #2922. Read from prod today:
I posted the decisive mechanism evidence on #2922: the
FluxInstanceCRD has noverifykeyunder
spec.sync(so the operator cannot declare it), and server-side-apply ownership showsflux-operatorowns onlyinterval/provider/ref/secretRef/url— neverf:verify— so a directpatch is both the only available mechanism and safe from pruning. The remaining defect is that
nothing writes it on the deploy path, now tracked as
ksail#6480.
Directions 2–4 (atomic publish-then-promote, DR signer path) remain open and are the substance of
this issue. Note a
scripts/validate-dr-signinggate already exists incd.yaml, so the DR halfhas moved too and should be re-scoped against it rather than built from the original description.
Suggested reframe: narrow this issue to the atomic publication guarantee (criteria 2 and 3 —
Flux cannot observe an artifact before signature and attestations complete, and a failed
push/sign/attest leaves the last verified artifact active), and let #2922 + ksail#6480 carry
criterion 1. Criterion 4 (DR from zero) is worth its own child now that the signing validator exists.
Not implementing the enforcement half here — I am shipping ksail#6480 this run.
devantler commentedon Aug 5, 2026
Triage: one criterion is already met, one is blocked, two remain implementable
I picked this up as the oldest actionable Security issue, investigated, and am releasing it rather
than holding a claim I am not delivering against this run. What I established:
Criterion 5 (static tests validate config nesting) is already satisfied.
scripts/validate-flux-verifyexists and is wired into CI (
go test ./scripts/validate-flux-verify). It checks the read pathspec.workload.flux.verify, reports strayverifykeys at any other path, and asserts the blockactually constrains a signer rather than merely being present. Its header comment cites this issue.
No work needed.
Criterion 1 (live root OCIRepository has non-null verification) is blocked, not unimplemented.
The repair already ships in the KSail prod deploys with (
reconcileFluxVerify, ksail#6454, first inv7.178.13; prod pins v7.178.14), but it is reachable only through
ksail cluster update, and thatstep is skipped on every prod deploy while the Cilium rollout gate is active. Full evidence and the
end-actionmarkers are in my comments on #2922; the live blocker is #2963.Criteria 2–4 (atomic publication, failed-push safety, DR signing path) are unaffected by that gate
and remain implementable now. They are the genuinely startable part of this issue: publish to an
immutable staging reference, sign and attest it, then repoint
latest, so Flux cannot observe anartifact before its supply-chain bundle is complete.
Suggest whoever picks this up next starts at criteria 2–4 and leaves criterion 1 to close on its own
once #2963 releases the gate — and re-reads the live resource then rather than assuming it applied.
devantler commentedon Aug 5, 2026
Revalidated against current
main(2c2d4c09)This issue was written on 2026-07-13. Four of its five acceptance criteria have since been delivered by other work, so the remaining scope is much narrower than the body suggests. Evidence for each, gathered by running the guards rather than reading them:
kubectl --context admin@prod get ocirepository/flux-system -n flux-system→spec.verifyempty,ref.tag=latestvalidatePublicationAction, andtest-verify-published-evidence.shpins the gate before promotion and after both attestationsvalidate-dr-signingrequires the DR job to grantid-token: write+attestations: write, to publish through the shared action, and its Fulcio identity to be in the cluster allow-listvalidate-flux-verify(nesting),validate-dr-signing(permissions/identity/ordering),test-verify-published-evidence.sh(gate wiring)Both guards pass at this head:
DR publication contract passed.andpassed=30 failed=0.Criterion 1 is not a config defect.
ksail.prod.yamlalready nests verification at the KSail-supportedspec.workload.flux.verify. The reason it does not reach the cluster is thatksail cluster update— the only code path that applies it — is skipped on every production deploy while the Cilium rollout gate is active. That is #2963's subject, so I have marked this issue blocked by it rather than restating the analysis here.One genuine gap surfaced while checking criterion 3, filed as #2967 and fixed in #2968: the contract proved each step existed in the right order but never that a step's failure stops the run, so a one-line
continue-on-error: trueonverify_evidencepassed every guard while discarding the evidence check. GitHub documents that key on composite steps as "allow[ing] the action to pass when this step fails", so the promotion would then have proceeded.Suggested disposition
Keep this issue open for criterion 1 only, or close it in favour of #2963 + #2922, which already track exactly that. No further work here is startable until the gate clears.
devantler commentedon Aug 6, 2026
Scope check before anyone implements this — two ACs are already met
Measured against
maintoday. Do not rebuild these:deploy-prodcomposite already publishes to animmutable staging reference, signs, attests and verifies before moving the mutable tag:
staging_reference → push_staging → cosign_sign → generate_sbom → attest_sbom → attest_provenance → verify_evidence → promote_latest. So Flux cannot observe an artifact before its signature andattestations exist, and a failed step leaves the previously promoted artifact active.
scripts/validate-flux-verifyhas unit tests and runs on every PR(
go test ./scripts/validate-flux-verify+go run … ksail.prod.yaml); it explicitly rejects thespec.cluster.verifymis-nesting described in the Problem section. The config inksail.prod.yamlis already at the supported
spec.workload.flux.verifypath.AC 1 (live root OCIRepository verified) is still unmet, and its blocker is now precisely known —
it is not the KSail config path and not #2951.
ksail cluster updateis the only step that wouldapply the config to the live resource, and it has been skipped for 11 days by the still-active
Cilium homogeneous-device rollout gate. Full evidence, including the skipped-step markers and the live
spec.verify=nullread, is in #2922:#2922 (comment)
Remaining genuine scope here: live enforcement (blocked on the rollout gate's completion/rollback
artifact) and the DR signed-artifact path.
devantler commentedon Aug 6, 2026
Re-measured against
maintoday — this has been delivered, criterion by criterionPicked up as the oldest open
Securityissue. Before implementing anything I checked whether itsevidence still holds, and it does not: the publication path was rebuilt after this was filed on
2026-07-13, and it now does what this issue asked for. Evidence per acceptance criterion, all read
from
main:2. "Flux cannot observe a new artifact until signature and required attestations are complete" — met.
.github/actions/deploy-prod/publish-platform-manifests/action.ymlno longer moveslatestfirst. Theorder is now: push to a unique staging tag (
staging-<sha>-<run>-<attempt>) → resolve the immutabledigest →
cosign signthat digest → attest SBOM and SLSA provenance (push-to-registry: true) →verify the published evidence → and only then
🚀 Promote the evidenced digest to latest.latest— the only reference production follows — is moved in the last step, after the bundle exists.
3. "Any failed push/sign/attest leaves the last verified artifact active" — met.
Promotion is the final step and every step runs under
set -euo pipefail, so any earlier failureaborts before
latestmoves; the previously promoted digest stays in place. The promote step alsore-reads
latestand fails if it does not resolve to the digest just evidenced, so a partialpromotion cannot pass silently.
The verification step is enforcing by default (
enforce-evidence-verification,default: "true") andneither caller overrides it — checked both
deploy-prod/action.ymlanddr-rebuild.yaml. It alsoasks the registry what it actually holds rather than inferring success from the steps having run,
which closes the "green step, no usable evidence" gap this issue worried about.
4. "DR can rebuild from zero without publishing unsigned bytes" — met.
dr-rebuild.yamlnow declaresid-token: writeandattestations: write, and calls the sameevidenced publish composite rather than pushing raw. Its identity is also in the intended allow-list:
dr-rebuild.yaml@refs/heads/mainis one of the three pinned verify subjects, so a DR-producedartifact verifies instead of being refused — the coupling this issue flagged is resolved on both ends.
1. "The live root OCIRepository has non-null Cosign verification with only the intended identities" —
met on the cluster, but not yet durable in Git.
Measured live just now,
ocirepository/flux-systemcarries exactly the three intended subjects. It isthere via a speculative merge-group deploy of #3001 rather than from
main, so the next successfuldeploy from
mainwould remove it again — see the correction on#3001 and the
mechanism filed as #3005.
Disposition
Closing as delivered. The one residual — making criterion 1 durable in Git — is not a separate piece
of work: it is exactly the scope of #2922, which is open with #3001 ready and parked on the
#2963 deploy blocker. Keeping this open alongside them tracks the same remaining change twice and
keeps a three-week-stale description of the publication path at the head of the security queue.
Nothing here was implemented by this run; this is a measurement of what already shipped.