Skip to content

Execute the digest rewrite the enforcing tier depends on - #243

Merged
stxkxs merged 1 commit into
mainfrom
execute-the-digest-rewrite
Sep 5, 2026
Merged

Execute the digest rewrite the enforcing tier depends on#243
stxkxs merged 1 commit into
mainfrom
execute-the-digest-rewrite

Conversation

@stxkxs

@stxkxs stxkxs commented Sep 5, 2026

Copy link
Copy Markdown
Member

Closes #221 (the execution half; see below for what #238/#241 already closed).

mutateDigest: true is the whole reason the enforcing tier enforces, and in this
repository it was a field value other files read rather than a behaviour anything
ran. Every Kyverno suite loads a BASE policy, where the field is false because
Kyverno rejects true under an Audit failure action — so the only rendition
carrying it was under no test at all.

scripts/check-digest-rewrite.sh renders each enforcing overlay and admits a real
signed ghcr.io/nanohype/* release through it, referenced by TAG, reaching the
registry and Rekor — the round trip admission makes. The overlays are enumerated
from the tree, so an overlay added later is executed too.

Both directions, on the real tree. Unmutated: two enforcing overlays each admit
the tagged pod carrying the digest verification resolved, exit 0. With the
production overlay's patch flipped to mutateDigest: false — a change every
YAML-reading gate still passes, because the field is simply gone rather than wrong —
it exits 1 naming the overlay. Kept as a probe in reverify-gates.sh (floor 35 → 37,
40/40).

How the result is observed is recorded in the script: the Kyverno CLI offers no
machine-readable channel carrying the object a verifyImages mutation admitted
(patchedResources is compared against every engine response and the rule produces
two; apply -o writes nothing for it; the JSON report omits the object). The reading
is the CLI's detailed report, with the expected patch supplied as the pod as
submitted
so a digest in the output can only have come from the engine. It fails in
the safe direction — a report that stops printing the token makes this red.

What it does not establish: that a cluster admits what the CLI admits.
docs/threat-model.md says so in those words, replacing a paragraph an earlier edit
had mangled into a duplicated fragment and an orphaned sentence. ci.yml carried a
claim this disproves — that kyverno test cannot reach Fulcio/Rekor — corrected.

The issue's second half is already closed. It asks for coverage derived from
scripts/ rather than a hand-written EXPECTED, and tests for
check-policy-admission.py and validate-dashboards.py. #238 landed
control_exempt_gates, the MAX_UNCOVERED_GATES ceiling counted off the filesystem,
and test modules for both.

Verification: ruff clean; mypy clean over 46 files; 476 tests / 18 modules;
controls.py 17; empty-corpus.py 30 probed gates; reverify-gates.sh 40/40;
task validate exit 0; check-named-things.py 196 references; yamllint and zizmor
clean.

`mutateDigest: true` is the whole reason the enforcing tier enforces.
Verification resolves a tag to a digest at admission; the kubelet resolves the
same tag again at pull time. Pinning at admission is what stops anyone who can
move a tag between those two moments from running an unverified image under a
policy reporting success.

In this repository that guarantee was a field value other files read, never a
behaviour anything ran. Every Kyverno suite loads a BASE policy, where the
field is false because Kyverno rejects true under an Audit failure action — so
the only rendition carrying it was under no test at all.
check-image-verification.py declines to assert the field and delegates the
pairing; check-policy-validity.py asserts Enforce implies true and then applies
a probe pod whose image is outside `ghcr.io/nanohype/*`, so the rule never
matches and the mutation path never runs.

──────────────────────── What now runs ────────────────────────

scripts/check-digest-rewrite.sh renders each ENFORCING overlay and admits a
pod through it — a real signed `ghcr.io/nanohype/*` release, referenced by TAG,
because a digest-pinned fixture needs no rewrite and would satisfy every check
against a policy that had stopped doing it. That fixture property is asserted
rather than assumed.

The overlays are enumerated from the tree, not named in the script. An overlay
added later is one more tier depending on a step, and it has to be executed
too.

It reaches the registry for the manifest and Rekor for the transparency-log
entry, which is the round trip admission makes — this exercises the path rather
than a model of it. A verification that could not REACH those is told apart
from a rewrite that did not happen: the first exits 2, because it observed
nothing.

──────────── How the result is observed, and why by report ────────────

The Kyverno CLI offers no machine-readable channel carrying the object a
verifyImages mutation admitted. Three were tried and each is recorded in the
script. `kyverno test`'s `patchedResources` is compared against EVERY engine
response for the rule, and a verifyImages rule produces two — one holding the
resource as submitted, one holding it mutated — so one comparison always diffs
whichever file is supplied, and supplying both produces a cross product with
two failures. `kyverno apply -o` writes a file for mutate rules and leaves it
empty for this one. The JSON policy report carries the verdict and the message
and not the object.

So the reading is the CLI's own detailed report. The expected patch supplied to
it is the pod AS SUBMITTED, and that direction is what makes the reading
non-circular: the mutated response differs from it, the report prints what the
engine produced, and a digest in that output can only have come from the
engine. An expectation carrying the digest would have the report echo the
fixture back.

That is text, and text is the weaker reading — but it fails in the safe
direction. The assertion is that one reference token appears, so a report that
stops printing it, wraps it, or renames the column makes the token absent and
this gate red. It cannot go quiet.

──────────────────────── What it does not establish ────────────────────────

That a cluster admits what the CLI admits. The engine is the same code and the
policy is the rendered one, and no environment reachable from this repository
runs an API server. docs/threat-model.md now says that in those words, in place
of a paragraph that had been mangled by an earlier edit into a duplicated
fragment and an orphaned sentence.

ci.yml carried a claim this work disproves — that `kyverno test` cannot reach
Fulcio/Rekor and never verifies a signature. It does; the reason the suites
never verified one is that they load base policies whose rule the fixtures do
not match. Corrected rather than left standing beside a step that contradicts
it.

──────────────────────── What proves it ────────────────────────

Both directions on the real tree. Unmutated: two enforcing overlays each admit
the tagged pod carrying the digest verification resolved, exit 0. With the
production overlay's patch flipped to `mutateDigest: false` — a change every
YAML-reading gate still passes, because the field is simply gone rather than
wrong — this exits 1 naming the overlay and what the tag then means. That is a
probe in scripts/tests/reverify-gates.sh, floor 38 → 40, and the harness runs
43 checks.

Registered in controls.py's NEEDS_NETWORK_SH rather than carrying a positive
control: the round trip IS what it executes, and a control for it offline would
be a control for a verification that did not happen.

──────────────────────── Verification ────────────────────────

ruff clean; mypy clean over 48 files; 493 tests across 19 modules; controls.py
18 controls; empty-corpus.py 31 probed gates; reverify-gates.sh 43/43; task
validate exit 0; check-named-things.py 196 references resolve; yamllint clean;
check-workflows.sh clean at MEDIUM and above.

Co-authored-by: stxkxsbot <275011021+stxkxsbot@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

CI Results

Check Status
Zero-placeholder gate ✅ success
YAML Lint ✅ success
Dashboard gate (live grafana.com ids + AMG-saveable) ✅ success
Kyverno policy tests (+ verify-images contract) ✅ success
Fork-safety gate (no hardcoded org, blocking) ✅ success
Helm-render (every addon × every env) ✅ success
Rendered images carry no unacknowledged CRITICAL ✅ success
Policy-admission (Enforce-tier Kyverno vs the fleet) ✅ success
ApplicationSet schema + sync waves ✅ success
Appset render (Karpenter subnet selector) ✅ success
Secret scan (gitleaks) ✅ success
Render + assert + schema + misconfig (all environments) ✅ success
Renovate manager coverage ✅ success
Loki volume warns before cutoff ✅ success
Falco loads every rule set it installs ✅ success
Gate scripts lint and typecheck ✅ success
Policy validity (kustomize + kyverno) ✅ success
The gates themselves are tested ✅ success
Pods name ServiceAccounts that exist ✅ success
Catalog CRs admissible against the operator chart ✅ success
Catalog source reads its revision ✅ success

All checks passed.

@stxkxs
stxkxs merged commit 7af9bb9 into main Sep 5, 2026
31 checks passed
@stxkxs
stxkxs deleted the execute-the-digest-rewrite branch September 5, 2026 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The digest rewrite the enforcing tier depends on is never executed

1 participant