The registry had two blockers. #59 fixed the first (nodes resolved the hostname to the non-hairpinned WAN and failed TLS against the router's CN=%D cert). This is the second: the pull is unauthenticated, and this registry refuses unauthenticated clients outright — measured 2026-10-05, its anonymous token carries access: null and every path 401s. pm-agent-registry is a dockerconfigjson Secret (SOPS, placeholders) referenced by imagePullSecrets on the dispatcher Deployment and on the committed worker Job template. It holds the SAME Gitea token as pm-agent-credentials, because the two consumers need two formats: the application reads a raw token from env, the kubelet reads a dockerconfigjson before any container starts. Same one-value-in-two-files pattern as the Keycloak client secret, documented in both .example files. The operator's part is the value plus one new scope: read:package. ADMISSION GAINED A SEVENTH RULE. imagePullSecrets is a pod-spec field no container references, which is exactly why it is easy to leave unread — the same shape of omission as initContainers, ephemeralContainers, env and envFrom before it. The kubelet reads the named Secret, in this namespace, before anything runs. Rule 1's pattern now REQUIRES the committed entry; rule 7 forbids any other and any second one. Both halves are needed: a foreach over an absent list yields no result. The leverage is small and the policy says so: the image is digest-pinned, so a different pull credential cannot change WHAT runs. Pinned anyway — 'only the committed shape' is the premise, and the rule costs four lines. kyverno test: 33 passed, 0 failed (was 28). Three new fixtures, each a real path: the committed entry plus a second one, a different Secret, and none at all. The gate also caught me shipping the Secret in plaintext — I never ran sops -e -i on the placeholder. Recorded in the handoff entry, because the file looked finished. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
In-cluster admission enforcement (scaffolding — Phase 2 / T-2.7)
Installs Kyverno and runs the same L0 policies as an in-cluster admission
controller, so a non-conforming resource that somehow bypassed CI still cannot be
admitted (defense in depth — docs/07, ADR-0011). Not applied in Phase 0 (no
cluster exists yet); this is scaffolding wired up in task T-2.7.
Enforcement COMPLEMENTS, never REPLACES, behavior tests
A policy admitting a manifest is not proof the runtime behaves (
docs/14principle 1, R-TEST-01). Kyverno confirms a resource is shaped correctly; it does not prove default-deny actually blocks a packet, that the VPN kill-switch fails closed, or that a PVC survives a rebuild. Those are proven only by the L2/L3 Chainsaw behavior and fault-injection tests (conformance/, ADR-0011), which remain mandatory. Admission is a gate, not a guarantee of behavior.
Authored once, shared
kustomization.yaml references the policy files in ../policies/ verbatim — the
identical objects the CI kyverno test gate validates — plus the Kyverno install.
No policy is forked between CI and admission, with one deliberate exception:
The one deliberate CI vs admission divergence — Secrets
../policies/disallow-plaintext-secrets.yaml (CI) requires a top-level sops block
on the Secret manifest. Flux decrypts SOPS before admission, so that block is
gone by the time the API server sees the Secret — the CI policy cannot run in-cluster.
The admission equivalent is secret-provenance.yaml, which enforces provenance
(Secret came from Flux / is an exempt controller Secret) instead of ciphertext. This
is escalated in the T-0.5 report because docs/07's literal wording ("reject a
plaintext Secret at admission") is not achievable post-decrypt.
Operational note (from ADR-0011)
Admission enforcement adds a failure mode: a bad policy can block legitimate
reconciliation. Mitigations: policies ship with their own kyverno test suite (this
directory's ../tests), roll out via GitOps, and the webhook failurePolicy: Fail
is chosen so the system fails closed (coordination-protocol.md #8) — validate for
bootstrap deadlocks in dev/test before prod.