An in-cluster consumer reaches a service by its *.svc.cluster.local name, which is by design a different path from how a client outside the cluster reaches the same service. pm-worker's host allowlist accepted TWO values: the cluster-local name and the external hostname gitea.home.keithjackson.dev. The second is removed. It was never needed — nothing sets it — it would route in-cluster traffic out through the Gateway and back for no reason, and as an ACCEPTED value it widened the allowlist to a host reachable over the namespace's world:443 egress rule, which is precisely the injection shape the allowlist exists to refuse. probe-validation.sh now pins that: 20 refusals, including the external hostname, the cluster-local name over https, and the cluster-local name on the wrong port. Also corrects a remark in the handoff entry: I cited cluster-vars' GATEWAY_VIP (192.168.1.53) as suspicious next to the nodes' 192.168.2.x. That was noise — those base cluster-vars values are dev-era leftovers for a decommissioned cluster and say nothing about prod. None of the measured facts depended on it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
7.9 KiB
date, branch, title
| date | branch | title |
|---|---|---|
| 2026-10-05 | docs/pm-agent-registry-pull-prereq | pm-agent reconciled exactly as designed — and is the first workload ever to pull from the in-cluster registry, which does not work yet |
What changed
Documentation only. No manifest, no policy, no code.
PR #57 merged and pm-agent reconciled. I measured what landed, and the measurement turned
up two prerequisites for the next operator step that nothing in this repository
declares — and that the handoff entry for #57 could not have known, because it listed the
image pull as unverified and that is exactly where it bit.
So this entry records the post-merge measurement, and
platform/pm-agent/README.md, docs/plans/pm-agent-issue-to-pr.md and
runbooks/pm-agent.md now name both prerequisites where they say "build, push, pin the
digest". Before this, that step read as one action; it is three, and the two new ones are
not an agent's to perform.
The finding is not specific to pm-agent. conformance-runner has the identical
registry path (gitea.home.keithjackson.dev/home-network/...), the identical "operator
pushes, a digest goes in a PR" model, and has never been pulled either — so the same two
blockers sit in front of the L2/L3 conformance wiring. Measured: no pod anywhere in the
cluster references that registry except the pm-agent dispatcher, which is therefore the
first workload ever to try it.
What I verified
Read-only, through the pinned toolbox, against the live prod cluster.
The merge reconciled, completely and exactly as designed.
GitRepository/flux-systemartifact revision:main@sha1:95597bd6883637…— the merge commit.platform-config,pm-agentandapps-wave-0all report that samelastAppliedRevisionwithReady=True. Compared field to field, per CLAUDE.md §2b, rather than assumed from the merge.- The security boundary is live.
ClusterPolicy/pm-agent-job-shape:validationFailureAction=Enforce,failurePolicy=Fail,Ready=True, and all six rules present (worker-must-be-the-committed-shape,worker-must-be-one-container-with-no-args,worker-volumes-must-be-workspace-or-tmp-emptydirs,worker-may-not-declare-init-or-ephemeral-containers,worker-env-names-must-be-the-committed-set,worker-may-not-use-envfrom). This is the check that mattered most: a ClusterPolicy added to the wrong kustomization list ships green and never reaches the cluster, which is what happened to the conformance boundary on 2026-09-29. - Namespace contents: both ServiceAccounts,
pm-agent-config(17 keys), thepm-agent-credentialsSecret, thedefault-denyNetworkPolicy, and both CiliumNetworkPolicies withVALID=True. - SOPS decryption works through the new overlay patch. Checked by KEY NAMES and VALUE
LENGTHS only, never values (CLAUDE.md §6): keys
gitea-token,claude-code-oauth-token, lengths 76 and 52 bytes — which are the base64 lengths of the committed placeholder strings, so the Secret holds the decrypted placeholders and not ciphertext. A ciphertext apply succeeds silently and would have leftENC[AES256_GCM,…]values an order of magnitude longer. pm-agent-workerreally is bound to nothing. The namespace has exactly one RoleBinding (pm-agent→ Rolepm-agent, subjectServiceAccount/pm-agent), and no ClusterRoleBinding anywhere names either pm-agent ServiceAccount.gitea-allow-pm-agent-ingressis applied in thegiteanamespace, matchingio.kubernetes.pod.namespace: pm-agent.- The dispatcher is
ImagePullBackOffon the all-zero placeholder digest — the designed, visible failure.
The two prerequisites, measured.
-
The nodes cannot verify the registry's TLS certificate. containerd's error on the pull is not about the digest:
failed to resolve reference "gitea.home.keithjackson.dev/home-network/agent-runner@sha256:000…": tls: failed to verify certificate: x509: certificate is not valid for any names, but wanted to match gitea.home.keithjackson.dev"not valid for any names" means the certificate presented to the node carries no matching SAN at all. From the operator's workstation the same name is healthy: it resolves to
192.168.2.20and serves a Let's Encrypt certificateCN=*.home.keithjackson.devwith SANs*.home.keithjackson.dev, home.keithjackson.dev— andCertificate gateway/wildcard-homeisReady=Truefor exactly those names. So the certificate that should be served is fine, and what the NODE reaches is not what the workstation reaches. Correcting the digest will not clear this. -
Anonymous pulls from this registry are refused. Not inferred from a bare 401 — that would have been wrong, because this Gitea answers 401 for everything anonymously:
/v2/home-network/agent-runner/manifests/latest,/v2/home-network/conformance-runner/…, a randomly-named nonexistent repository, andnosuchowner/nosuchimageall return 401, and/api/v1/packages/Home-Networkreturns{"message":"token is required"}. The registry does issue a token to an anonymous client, but itsaccessclaim isnull— no pull scope. So the conclusion that holds is the simple one: an unauthenticated client gets nothing from this registry, so a pull with no credential cannot succeed. Andgrepover the tree finds noimagePullSecretsanywhere — not inplatform/pm-agent/deployment.yaml, not ininfra/agent/worker-job-template.yaml, not inconformance/incluster/.
What I did NOT verify
-
Why the node sees a nameless certificate. I measured that it does, and that the LAN does not. I did not diagnose it: I did not read the nodes' resolver or
/etc/rancher/k3s/registries.yaml(node files, not reachable read-only through the API), and I did not check whether the node resolves the name to the same192.168.2.20the workstation does or to something else.A correction to a remark I first wrote here: I cited
cluster-vars'sGATEWAY_VIP: 192.168.1.53as suspicious next to the nodes'192.168.2.x. That was noise — the operator confirms those basecluster-varsvalues are dev-era leftovers for a cluster that is decommissioned, so the 192.168.1.x addresses in that file say nothing about prod and should not be read as evidence about anything. The measured facts stand on their own and none of them depended on that knob. -
Whether the package would be pullable once authenticated. Nothing has ever been pushed to that path, so "a credential is sufficient" is untested. The only thing established is that no credential is insufficient.
-
Which fix the operator wants. Two routes, and they are not equivalent: (a) an
imagePullSecret— adockerconfigjsonholding a Gitea token, SOPS-encrypted likepm-agent-credentials, referenced from the Deployment and from the committed worker Job template; note the Job template's shape is pinned by admission, andimagePullSecretsis a field that policy does not currently constrain, so adding one is a template change that needs a deliberate look at whether the policy should pin it too. (b) allowing anonymous reads of that package in Gitea — no manifest change at all, but it makes the image world-readable to anything that can reach the host. I have implemented neither: both need an operator action I must not take, and (b) is a posture decision. The docs now present both. -
Everything the #57 entry listed as unverified about RUNTIME remains unverified. The image has still never been built or pushed. No dispatcher has authenticated to Gitea, no issue has been triaged, no worker Job has been created — so the admission policy, though now provably applied and Enforce, has still never refused or admitted a real Job. The network policies have not carried traffic.
-
I did not re-run the merge gate against
mainafter the merge. This branch's own gate run is the evidence for this change; the merged state's is CI run on PR #57.