feat(pm-agent): the kubelet's pull credential, and the seventh field admission had not read
gate / gate (pull_request) Failing after 1m53s
gate / conformance (pull_request) Has been skipped

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]>
This commit is contained in:
jakman85andClaude Opus 5 committed 2026-10-06 01:03:54 -05:00
1 parent 523f482957
commit 7fa5e84d25
13 files changed
+413 -21

No files matched your search

+2
View File
@@ -922,6 +922,8 @@ def selftest_template(path: str) -> None:
assert pod.get("automountServiceAccountToken") is False, \
"template must not mount a ServiceAccount token"
assert pod.get("restartPolicy") == "Never", "a retried agent would double-push"
assert [x.get("name") for x in pod.get("imagePullSecrets") or []] == ["pm-agent-registry"], \
"the template must name exactly the committed pull secret (admission pins it)"
assert len(pod.get("containers") or []) == 1, "exactly one container"
c = pod["containers"][0]
assert c.get("command") == ["/usr/local/bin/pm-worker"], "command must be the pinned entrypoint"
+7
View File
@@ -68,6 +68,13 @@ spec:
# guarantee it cannot use one is for it not to have one.
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
# The kubelet's pull credential, the same one the dispatcher uses. Pinned by the
# admission policy to exactly this name: `imagePullSecrets` is a pod-spec field, so a
# Job could otherwise name any Secret in the namespace and have the kubelet read it
# before a container ever starts. The image is digest-pinned, so the leverage is small
# — but "only the committed shape" is the rule this template exists to express.
imagePullSecrets:
- name: pm-agent-registry
securityContext:
runAsNonRoot: true
runAsUser: 65532