Repository navigation
Root OCIRepository verification does not survive onto the live cluster #2938
Description
🤖 Generated by the Agentic Engineer
Evidence
The root Flux source on prod has no signature verification, and this survives a full successful deploy against a correctly-configured repo.
flux-system/flux-system url=oci://ghcr.io/devantler-tech/platform/manifests
ref.tag=latest interval=1m0s spec.verify = null
Every tenant OCIRepository in the same cluster (ascoachingogvaner, aws, doggy-countdown, github-config, wedding-app) does carry a cosign verify block, so verification works here — the root source specifically lacks it.
This is not a pending rollout:
- #2919 moved the config to
spec.workload.flux.verifyon 2026-08-03T03:26Z, andyqconfirms it resolves there today with all three intended identities.spec.cluster.verifyisnull. - The
🚀 Deploy to Prodjob succeeded at 2026-08-03T23:17:36Z (run30861750366) — about 20 hours later. - The root source was still
spec.verify: nullwhen re-read on 2026-08-04T00:2xZ.
Where the value is lost
The live FluxInstance (flux-system/flux) has no verify key under spec.sync; its sync block is exactly interval, kind, path, provider, pullSecret, ref, url. flux-operator derives the root OCIRepository from that instance, so a sync without verify yields a root source without spec.verify.
Both KSail halves exist, which is why this reads as a defect rather than a gap:
- input modelled —
Workload.Flux.Verify(FluxVerifySpec),pkg/apis/cluster/v1alpha1/types.go:285, documented as "rendered onto the generated flux-system OCIRepository" - renderer present —
pkg/svc/installer/flux/ocirepository_patcher.go(buildVerifyPatch/applyVerify/ensureVerify) - platform CI pins KSail 7.178.13
Hypotheses to discriminate (not yet proven)
- Create-time only.
ensureVerifyruns on a path the prod deploy does not take, so an existing cluster never receives it. Same class as thefloatingIPEnabledno-op in feat: enable the Hetzner floating-IP stable API endpoint on prod #2524: static validation green, production effect nil. - Applied then reverted. flux-operator owns that
OCIRepository; a direct patch withFluxInstance.spec.sync.verifyunset is liable to be reconciled back to null. Relevant: KSail's installer-sideSyncstruct (pkg/svc/installer/flux/fluxinstance_types.go:132) models eight fields and has noVerify, so KSail cannot currently expresssync.verify— the same typed-model-narrower-than-the-CRD class as ksail#6455.
Acceptance criteria
- The mechanism is identified with evidence — a deploy-log read showing whether
ensureVerifyexecuted, plus a re-read ofspec.verifyimmediately after a deploy to detect an apply-then-revert. - The live root
OCIRepositoryhas a non-null cosignverifycarrying only the three intended production identities. - The value survives a subsequent flux-operator reconcile and a subsequent prod deploy — verified by re-reading after both, not by observing it once.
- A regression fails if root verification is absent, asserted against live or rendered shape, not against the source YAML — the whole point of this issue is that a correct source file did not produce a verified root.
- If the root cause is in KSail, the fix lands there and platform pins the version carrying it.
Out of scope
Publication atomicity (#2930) and the DR signer path (#2840) — both already merged.
Rough size: small-to-medium — likely one KSail fix plus a version bump, but the discrimination step comes first and may redirect it.
Part of #2627.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Effort
Projects
- StatusShow more project fields✅ Done
Activity
coderabbitai commentedon Aug 4, 2026
🔗 Related PRs
#2319 - feat(security): cosign-verify the infrastructure Flux OCI artifact [merged]
#2625 - fix(deploy): synchronize production GHCR pull auth [merged]
#2633 - fix(deploy): verify incoming image on Talos nodes [merged]
#2635 - fix(deploy): reboot stale nodes so containerd adopts refreshed GHCR auth [merged]
#2919 - fix(security): move Flux cosign verify to the path KSail reads [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!
devantler commentedon Aug 4, 2026
Both hypotheses are refuted — it is a config gap here, not a KSail defect
Discriminated read-only on
admin@prodvia SSA field ownership, which settles it without touching anything:Hypothesis 1 — create-time-only no-op (the #2524 floating-IP class): FALSE.
ksailis a field manager on this object, with anUpdateat23:21:35Z— during the🚀 Deploy to Prodthat succeeded at 23:17:36Z. It ran.Hypothesis 2 — applied, then reverted by flux-operator: FALSE.
flux-operator's only Apply is 2026-07-13, three weeks before ksail's update. Nothing overwrote anything, and thesource-controllerupdate 2s later is the artifact/status write.What ksail actually owns is the whole story:
{"f:metadata":{"f:annotations":{"f:reconcile.fluxcd.io/requestedAt":{}}}}It owns only the reconcile-trigger annotation. It never touched
specat all — so it did not setspec.verifyand did not fail to; it was never asked to.Root cause:
spec.workload.verifyis absent from this repo'sksail.yaml.ensureVerifyIfConfigured(pkg/svc/installer/flux/resources.go:368) patchesspec.verifyonly "when the user has configured it", so with noverify:key the call short-circuits beforeensureVerify.grep -n verify ksail.yaml→ no match; theworkload:block carries onlysourceDirectory,kustomizationFile,validation.KSail is not at fault: it models the input (
Workload.Flux.Verify), renders the patch (ocirepository_patcher.go), and CI pins a version that has both.Setting
verify:makes Flux verify signatures at pull time on the root source. If root publication is not already producing signatures the cluster can verify, the rootOCIRepositorystops reconciling — a cluster-wide GitOps outage, not a degraded control.So the ordering is: prove root artifacts are signed and verifiable first, then enable
verify:— which is what #2922 ("configured but not enforced") is about, and why this belongs behind it rather than beside it. I have deliberately not shipped the config change on this evidence alone.Suggested next step: re-scope this issue from "why is the renderer a no-op" (answered: it is not) to "add
spec.workload.verifytoksail.yaml, gated on root signing being verified", and mark it blocked by #2922.devantler commentedon Aug 4, 2026
Mechanism identified — it is neither hypothesis
ksail cluster updatenever runs on prod. The step that invokes it is conditionally skipped, and has been on every prod deploy for at least four days. No KSail version can fix this, because the code that would apply the verify block is never executed.Evidence
.github/actions/deploy-prod/action.ymlgates the step that runs./scripts/run-ksail-prod-with-pull-auth.sh cluster update:In the most recent prod deploy (merge_group run 30927795575, 2026-08-04T16:09Z) the log shows which arm failed:
wait_flux_revisionoutcome=success✅ (second arm satisfied)cilium_rollout_gateCilium homogeneous-device rollout gate active: Cluster Autoscaler is suspended.⇒active == 'true'❌__self.cluster_updateoutcome=skipped; duration_ms=0Not a one-off —
cluster_updateoutcome across prod deploys:cluster_update30927795575)30924710728)30795581205)30696960482)The gate is still active right now:
kube-system/cluster-autoscaler-hetzner-cluster-autoscalerreads0/0on prod.Why the version pin was a red herring
ksail #6454 ("re-assert Flux artifact verification on cluster update") merged 2026-08-03T19:29Z. I verified containment rather than inferring it from dates —
compare/v7.178.13...56bb93bareturnsidentical, so the fix shipped in v7.178.13, and livemainalready pins 7.178.14 across all four call sites. The fix is present and pinned; the command that carries it is skipped.Both hypotheses in the body are now closed
flux-operatorapplies the rootOCIRepositorywithoutf:verify, so a patch is never reconciled out. (Also worth correcting the body's premise: theFluxInstanceCRD has nospec.sync.verifyproperty, so the "KSail cannot expresssync.verify" framing describes the CRD, not a KSail typed-model gap — KSail patches theOCIRepositorydirectly.)The larger problem this uncovered
The gate's own comment calls it temporary and says a "reviewed rollout-completion or rollback artifact removes the gate". It has instead been suppressing
ksail cluster updatecontinuously for ≥4 days, and that step is the platform's only Talos machine-config sync. So the exposure is much wider than this issue: machine config has not been reconciled on prod since at least 2026-08-01, silently, with a green deploy each time.Filing that separately rather than widening this issue — a temporary safety gate that indefinitely disables an unrelated reconcile loop, with no signal, is its own defect.
What this changes for the acceptance criteria
The first criterion (mechanism identified with evidence) is met by the above. The remaining criteria cannot be satisfied while the gate is active, because the applying command does not run — so the fix here is not a KSail change. It is either releasing the Cilium gate, or decoupling machine-config sync from it.
Leaving this claimed while I file the companion issue.
devantler commentedon Aug 6, 2026
Still true two days later — and it has now survived ~109 successful merge-queue runs
Re-measured live on 2026-08-06 ~06:15Z. Nothing has changed, and the control still holds cleanly:
Every tenant source in the same cluster still carries a full cosign block — 5 of 5:
ascoachingogvaner,aws,doggy-countdown,github-config,wedding-app, each withprovider: cosign+matchOIDCIdentity. So the mechanism demonstrably works in this cluster; theroot source is the single one without it.
What is new since this was filed: there have been 109 successful
merge_groupCI runs between2026-08-04T00:26Z and now. The original evidence rested on the value not surviving one deploy; it
has now not survived a hundred. (Stated precisely: those are successful merge-queue CI runs — the
🚀 Deploy to Prodjob is path-gated, so not every one of the 109 reconciled prod. The point is thatno amount of ordinary successful delivery is converging on the declared state.)
One correction for whoever picks this up
spec.verifyon the root is not observable from--branch mainrun history. The deploy job runson the
merge_groupevent, so agh run list --branch mainsweep shows zero deploys and reads as"nothing has shipped" — it hasn't; 128 main runs in the window are
CodeQL/Release/TODOs/Validate Mainonly. Query--event merge_groupinstead.flux-system/aws-packageis alsoverify=NULL, but that is a third-party upstream(
ghcr.io/flux-iac/aws-primitive-modules) pinned to an immutable tag — a different risk shape, andout of scope here.
The security asymmetry is the part worth holding onto: the one source that is not verified is the
root of trust that deploys everything else, including all five verified tenants.
devantler commentedon Aug 6, 2026
Consolidating into #2922 — same defect, and this issue had the better mechanism
#2960 is now decided,
and settling it made clear that this issue and #2922 are the same defect described from two angles:
the root
flux-systemOCIRepository has nospec.verify, and a fully successful prod deploy does notchange that.
Credit where it is due: this issue's diagnosis was the more accurate of the two. Its "Where the
value is lost" section already identified that flux-operator derives the root OCIRepository from
FluxInstance.spec.sync, and that asyncwithoutverifytherefore yields a source withoutspec.verify. That is exactly right, and it is confirmed:spec.synchas 8 properties and the stringverifyappears zero times in the whole v1 CRD schema. #2922 instead attributed it to a KSailpatch on the install-only path, which live field ownership shows is not what happens (KSail's only
field-manager entry on that resource is the
reconcile.fluxcd.io/requestedAtannotation — no spec key).That correction has been carried into #2922, along with this issue's evidence that the gap survives a
successful deploy.
Keeping #2922 as the single tracking issue because it is older, is typed
Security(correct for asupply-chain control, where this one is typed
Bug), and carries the acceptance criteria and the nowamended mechanism. Nothing from here is lost:
🚀 Deploy to Prodgreen at 2026-08-03T23:17:36Z, root sourcestill
spec.verify: nullon 2026-08-04) → the durability evidence on Cosign verify is configured but not enforced: the deploy path never applies it to the live OCIRepository #2922;verifyand areReady=True) → cited onCosign verify is configured but not enforced: the deploy path never applies it to the live OCIRepository #2922 and in Spike: can a non-operator field manager hold spec.verify on the root OCIRepository? #2960 as evidence the mechanism functions in this cluster.
Closing as a duplicate of #2922. Reopen if you would rather track the observed symptom separately from
the fix.