Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ jobs:
- name: πŸ” Validate Flux verification is in effect
run: |
go test ./scripts/validate-flux-verify
go run ./scripts/validate-flux-verify ksail.prod.yaml
go run ./scripts/validate-flux-verify \
ksail.prod.yaml \
k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml

# .github/actionlint.yaml silences actionlint's "unexpected key" diagnostic
# for concurrency.queue, which also removes its enum check. This restores it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,69 @@ spec:
options:
- name: ndots
value: "1"
# Enforce cosign signature verification on the ROOT source β€” the
# flux-system OCIRepository every controller, tenant binding and policy
# arrives through. Without this the root artifact is pulled from a mutable
# tag with nothing checking who produced it.
#
# PATCH THROUGH THE OPERATOR, not from the cluster config and not from a
# hand-written OCIRepository override. flux-operator generates this
# resource and is its Apply field manager, owning
# url/ref/interval/provider/secretRef. Two consequences, both measured
# live: KSail's spec.workload.flux.verify (ksail.prod.yaml) is applied
# only on the bootstrap path β€” on a routine deploy KSail writes no spec
# key on this resource at all, so that setting never reaches an existing
# cluster; and a full in-repo override would contend with the operator for
# those five fields on the root source, where losing the reconcile fight
# stops all GitOps delivery. A kustomize patch avoids both: the operator
# itself writes the field, so it lands under the manager that already owns
# the resource β€” no second writer, no field contention, no SSA pruning
# question. This is flux-operator's documented route for exactly this
# case ("OCI sync with Cosign verification").
#
# The subjects are pinned to the three workflows that legitimately publish
# this artifact, on the refs they publish from, rather than `@.+` β€” so a
# signature these same workflows produce on any other ref is not trusted:
# ci.yaml on the merge-queue ref β€” automatic deploy on merge
# cd.yaml on main β€” manual workflow_dispatch deploy
# dr-rebuild.yaml on main β€” disaster recovery, which republishes
# this artifact while rebuilding prod
# from zero; omitting it would leave a
# recovery producing a correctly-signed
# artifact that Flux still refuses.
# They are kept identical to spec.workload.flux.verify in ksail.prod.yaml,
# which covers the bootstrap path; scripts/validate-flux-verify asserts
# both halves on every pull request.
#
# ⚠️ A MISTARGETED KUSTOMIZE PATCH RENDERS NOTHING, EXITS 0 AND WARNS
# NOBODY (measured with kubectl kustomize on this file). A typo in the
# target below yields a security control that is present here, absent from
# the cluster, and green in CI. scripts/validate-flux-verify checks the
# target, and the effect is confirmed by reading the live resource back:
# kubectl get ocirepository/flux-system -n flux-system -o yaml | grep -A5 verify
- target:
kind: OCIRepository
name: flux-system
namespace: flux-system
patch: |
- op: add
path: /spec/verify
value:
provider: cosign
# πŸ”΄ EXACTLY ONE ENTRY. cosign's keyless verification rejects a
# multi-entry matchOIDCIdentity outright β€” "unsupported: multiple
# identities are not supported at this time" β€” and it fails CLOSED
# for the whole set, so a list verifies nothing while looking
# stricter than a single matcher. Flux documents the field as an
# OR'd list and the schema accepts one, so a list is both
# docs-valid and schema-valid and still refuses every artifact.
# Alternation therefore lives INSIDE the subject regex.
# The three trusted signers, in order: ci.yaml (automatic prod
# deploy, signs under the merge-queue ref), cd.yaml (manual
# workflow_dispatch from main), dr-rebuild.yaml (disaster recovery
# from main, which republishes this same artifact).
# scripts/validate-flux-verify fails the build on any list longer
# than one, so this cannot regress silently.
matchOIDCIdentity:
- issuer: '^https://token\.actions\.githubusercontent\.com$'
subject: '^https://github\.com/devantler-tech/platform/\.github/workflows/(ci\.yaml@refs/heads/gh-readonly-queue/main/.+|(cd|dr-rebuild)\.yaml@refs/heads/main)$'
77 changes: 43 additions & 34 deletions ksail.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -199,50 +199,59 @@ spec:
tag: latest
flux:
# Cosign keyless signature verification for the flux-system OCIRepository.
# KSail reads this at spec.workload.flux.verify (ksail#4987) and applies it
# to that resource. The manifests artifact is cosign-signed keyless in the
# deploy-prod composite (push β†’ sign β†’ attest), and the subjects below are
# pinned to the production deploy refs rather than `@.+`, so a signature
# these workflows might produce on any other ref is not trusted.
# KSail reads this at spec.workload.flux.verify (ksail#4987). The manifests
# artifact is cosign-signed keyless in the deploy-prod composite
# (push β†’ sign β†’ attest), and the subjects below are pinned to the
# production deploy refs rather than `@.+`, so a signature these workflows
# might produce on any other ref is not trusted.
#
# ⚠️ NOT REACHING THE RUNNING CLUSTER β€” verified 2026-08-04: the live
# ocirepository/flux-system carries no spec.verify. #2922 tracks it.
# The cause is NOT this config: the deploy's `ksail cluster update` step is
# being skipped entirely by the Cilium rollout gate (#2951 β€” `outcome=skipped
# duration_ms=0` on every prod deploy for days), so the command that would
# apply this has not run. Once #2951 releases that gate this may simply
# start working β€” flux-operator owns url/ref/interval/provider/secretRef on
# that resource but NOT verify, and Server-Side Apply lets it prune only
# what it owns, so a verify written by KSail should survive.
# Do NOT read it as an active control until you have checked:
# kubectl get ocirepository/flux-system -n flux-system -o yaml | grep -A5 verify
# THIS BLOCK COVERS BOOTSTRAP ONLY β€” the steady-state control lives in the
# FluxInstance. KSail applies verification from its Flux install path, so
# this is what a cluster gets when it is CREATED. On an existing cluster
# KSail writes no spec key on that resource at all: measured live,
# flux-operator is its Apply field manager (owning
# url/ref/interval/provider/secretRef) while KSail's only field-manager
# entry is the reconcile-trigger annotation. The control that reaches a
# RUNNING cluster is therefore the kustomize patch in
# k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml
# which has the operator itself write spec.verify. Keep the two matcher
# lists identical; scripts/validate-flux-verify asserts both halves on
# every pull request.
#
# DO NOT "fix" this by hand-writing an in-repo OCIRepository override: a full
# override contends with the operator for those five fields on the ROOT
# source, where losing the reconcile fight stops all GitOps delivery.
#
# Signature enforcement that IS live today: tenant/app OCIRepositories carry
# their own spec.verify, and container images are checked at the Talos
# containerd pull layer plus the Kyverno image-verification policy. The
# infra source is the remaining gap (#1570).
# Signature enforcement elsewhere: tenant/app OCIRepositories carry their
# own spec.verify, and container images are checked at the Talos containerd
# pull layer plus the Kyverno image-verification policy.
verify:
provider: cosign
# πŸ”΄ EXACTLY ONE ENTRY. cosign's keyless verification rejects a
# multi-entry matchOIDCIdentity outright β€” "unsupported: multiple
# identities are not supported at this time" β€” and fails CLOSED for the
# whole set, so a list verifies nothing while looking stricter than a
# single matcher. Flux documents the field as an OR'd list and the schema
# accepts one, so a list is both docs-valid and schema-valid and still
# refuses every artifact. Alternation lives INSIDE the subject regex.
#
# This is the BOOTSTRAP and DISASTER-RECOVERY path, so a broken matcher
# here stays dormant until the moment it is least affordable: a DR
# rebuild would produce a correctly-signed artifact that Flux then
# refuses, with no cluster left to work around it from.
#
# The three trusted signers, alternated in the subject below:
# ci.yaml automatic prod deploy; deploy-prod runs on
# merge_group, signing under the merge-queue ref
# refs/heads/gh-readonly-queue/main/<pr>
# cd.yaml manual prod deploy; workflow_dispatch from main
# dr-rebuild.yaml disaster recovery; workflow_dispatch from main,
# republishing this same artifact
# cd and dr-rebuild are both pinned to refs/heads/main, so a run
# dispatched from any other ref is not trusted.
matchOIDCIdentity:
# Automatic prod deploy: ci.yaml deploy-prod runs on merge_group, which
# signs under the merge-queue ref refs/heads/gh-readonly-queue/main/<pr>.
- issuer: '^https://token\.actions\.githubusercontent\.com$'
subject: '^https://github\.com/devantler-tech/platform/\.github/workflows/ci\.yaml@refs/heads/gh-readonly-queue/main/.+$'
# Manual prod deploy: cd.yaml is workflow_dispatch, run from main.
- issuer: '^https://token\.actions\.githubusercontent\.com$'
subject: '^https://github\.com/devantler-tech/platform/\.github/workflows/cd\.yaml@refs/heads/main$'
# Disaster recovery: dr-rebuild.yaml is workflow_dispatch, run from main,
# and republishes this same artifact while rebuilding prod from zero.
# Without this entry a recovery produces a correctly-signed artifact that
# Flux still refuses β€” the one situation where a manual workaround is
# least available. Pinned to refs/heads/main, like cd.yaml above, so a DR
# run dispatched from any other ref is not trusted.
- issuer: '^https://token\.actions\.githubusercontent\.com$'
subject: '^https://github\.com/devantler-tech/platform/\.github/workflows/dr-rebuild\.yaml@refs/heads/main$'
subject: '^https://github\.com/devantler-tech/platform/\.github/workflows/(ci\.yaml@refs/heads/gh-readonly-queue/main/.+|(cd|dr-rebuild)\.yaml@refs/heads/main)$'
validation:
# Parity with ksail.yaml: the prod kustomization pulls the same shared
# bases (Coroot + Flagger), so `ksail --config ksail.prod.yaml workload
Expand Down
Loading
Loading