Files
home-network-k3s-impl/platform/pm-agent/pm-agent-registry.sops.yaml.example
jakman85andClaude Opus 5.5 7f0cbc7ba2
gate / gate (pull_request) Failing after 2m56s
images / images (pull_request) Successful in 32s
gate / conformance (pull_request) Has been skipped
feat(pm-agent): pin the first published agent-runner digest, start dry, and fix the token scopes
Rebased #61 onto the CI image pipeline (#81). This commit makes the remaining in-git
blockers to going live go away:

- deployment.yaml and PM_WORKER_IMAGE pin images run 340's digest (sha256:c4eba5b3...,
  tag sha-523cd9118f95 = main), read from the registry. Renovate markers unchanged.
- PM_DRY_RUN=true and PM_REQUIRE_LABEL=agent for the first run. Dry run is required, not
  just cautious: the run-340 image's baked worker template predates imagePullSecrets, so
  admission would deny its Jobs until CI re-publishes after this merge and Renovate bumps.
- Token scopes: Gitea 1.24.3 scopes GET /api/v1/user (the dispatcher's first call) under
  read:user. The documented list was missing it. Now read:user, write:repository,
  write:issue, read:package everywhere.
- Org-package read documented as a team (Packages=Read, no repos), not just a scope.
- sops is toolbox-only; the .examples say so and give the in-place edit.
- runbook: key-name check no longer emits Secret values before the pipe.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:16:21 -05:00

119 lines
6.8 KiB
Plaintext

# =============================================================================
# TEMPLATE — NOT APPLIED, NOT ENCRYPTED (`.yaml.example` is ignored by kustomize and is
# never auto-encrypted by SOPS). Reference scaffold + OPERATOR HAND-OFF.
#
# WHAT THIS IS: the credential the KUBELET uses to pull `agent-runner` from the in-cluster
# Gitea registry. It is NOT the credential the agent uses to talk to Gitea — that one is
# `pm-agent-credentials` and it is read by the application. Two Secrets, because they are
# consumed by two different things in two different formats:
#
# pm-agent-credentials Opaque read by pm-dispatch / pm-worker as env vars
# pm-agent-registry dockerconfigjson read by the KUBELET, before any container runs
#
# WHY A PULL CREDENTIAL IS NEEDED AT ALL. Measured 2026-10-05: an unauthenticated client
# gets nothing from this Gitea's registry — it issues an anonymous token whose `access`
# claim is `null`, and every path answers 401 whether it exists or not
# (`/api/v1/packages/Home-Network` replies `{"message":"token is required"}`). So a pull
# with no credential cannot succeed, and before this Secret there was no `imagePullSecret`
# anywhere in the tree. See decisions/2026-10-05-pm-agent-registry-pull-prereq.md.
#
# THE OTHER HALF, ALREADY FIXED: the nodes used to resolve this registry's hostname to the
# non-hairpinned WAN address and fail TLS against the router's `CN=%D` placeholder
# certificate. That is the `/etc/hosts` pin in
# infra/ansible/inventory/group_vars/k3s_prod/vars.yml
# (decisions/2026-10-06-pm-agent-node-resolves-gitea-to-wan.md). Both halves are needed; a
# pull fails on whichever is missing.
#
# =============================================================================
# HAND-OFF STEPS (operator)
# =============================================================================
#
# (1) USE THE SAME `pm-agent` ACCOUNT the agent already has (created per
# pm-agent-credentials.sops.yaml.example — the dedicated, NON-ADMIN one). A second
# account would be a second thing to keep non-admin for no gain: this token reads one
# package and nothing else.
#
# (2) ADD THE PACKAGE SCOPE. Gitea token scopes are fixed at creation, so this means
# generating a token with all four scopes and using it in BOTH Secrets:
#
# read:package <- NEW, what the kubelet needs to pull
# read:user <- the dispatcher's startup GET /user (Gitea 1.24.3 scopes it)
# write:repository <- the agent's clone / branch push / pull-request create
# write:issue <- the agent's issue comments
#
# THE SCOPE IS NOT THE PERMISSION. `read:package` only lets the token use whatever
# package access the ACCOUNT has, and these packages belong to the ORGANISATION
# (`home-network/…`), not to a repository. Gitea 1.24.3 decides org-package access in
# services/context/package.go `determineAccessMode`: the highest Packages-unit mode of
# any org team the user is in, else READ if the org is visible to that user. Home-Network
# is `public` today (measured 2026-10-30), so a signed-in non-member already gets read —
# but that is a side effect of a visibility setting nobody chose for this. Make it
# explicit, and independent of visibility:
#
# Organisation Home-Network -> Teams -> New Team
# name: pm-agent-pull permission: none (unit-level)
# repositories: NONE (do not tick "all repositories")
# units: Packages = Read; every other unit = No Access
# then add `pm-agent` as its only member.
#
# That grants package read on the org and nothing on any repository; the repo `Write`
# comes only from the collaborator entry in the credentials template.
#
# Gitea -> (as pm-agent) Settings -> Applications -> Generate New Token.
# Put the SAME value in `pm-agent-credentials` (`gitea-token`) and in the
# `.dockerconfigjson` below. One token, two consumers, two formats — the same
# one-value-in-two-files pattern the Keycloak client secret already uses here, and for
# the same reason: a Secret cannot be shared across formats.
#
# (3) BUILD THE DOCKERCONFIGJSON. `auth` is base64 of `<username>:<token>` — NOT a hash,
# just encoding, so treat this file exactly as secret as the token itself:
#
# USER=pm-agent
# TOKEN='<the token from step 2>'
# AUTH=$(printf '%s:%s' "$USER" "$TOKEN" | base64 -w0)
# cat > /tmp/dockerconfig.json <<JSON
# {"auths":{"gitea.home.keithjackson.dev":{"username":"$USER","password":"$TOKEN","auth":"$AUTH"}}}
# JSON
#
# THE REGISTRY KEY MUST BE THE HOST AS THE IMAGE REFERENCE SPELLS IT —
# `gitea.home.keithjackson.dev`, no scheme, no port, no path. The kubelet matches the
# pull host against these keys literally; `https://…` or a trailing `/v2/` silently
# matches nothing and the pull is attempted anonymously, which 401s exactly as if this
# Secret were absent.
#
# (4) COPY, FILL, ENCRYPT, COMMIT:
#
# `sops` IS IN THE PINNED TOOLBOX, NOT ON THE HOST (measured 2026-10-30: absent from the
# workstation's PATH; sops 3.9.4 and vi in the toolbox). The simplest path needs no
# plaintext file at all — edit the committed, already-encrypted file in place, from a
# worktree, the way runbooks/vpn-remote-access.md does for its key:
#
# ./infra/devcontainer/dev.sh run sops platform/pm-agent/pm-agent-registry.sops.yaml
#
# It opens vi on the decrypted document; replace the REPLACE-ME value(s), save, quit, and
# sops re-encrypts on write. If you take the cp route below instead, run the `sops -e -i`
# line through `./infra/devcontainer/dev.sh run` as well, and delete any plaintext temp file.
#
# cp platform/pm-agent/pm-agent-registry.sops.yaml.example \
# platform/pm-agent/pm-agent-registry.sops.yaml
# # paste the one-line JSON from step 3 as the .dockerconfigjson value
# sops -e -i platform/pm-agent/pm-agent-registry.sops.yaml
#
# The root `.sops.yaml` rule matches this path and encrypts `stringData`, so the diff
# stays reviewable. NEVER `sops -d` it into a terminal or a transcript (CLAUDE.md §6).
# It is already listed under `resources:` in platform/pm-agent/kustomization.yaml.
#
# (5) ROTATION. Rotating the Gitea token means updating BOTH Secrets, because it is one
# token. The dispatcher must then be restarted to re-read its env; worker Jobs and the
# kubelet pick the new value up on their own.
# =============================================================================
apiVersion: v1
kind: Secret
metadata:
name: pm-agent-registry
namespace: pm-agent
type: kubernetes.io/dockerconfigjson
stringData:
# One line. Registry key = the host exactly as the image reference spells it.
.dockerconfigjson: '{"auths":{"gitea.home.keithjackson.dev":{"username":"pm-agent","password":"REPLACE-ME-the-same-gitea-token","auth":"REPLACE-ME-base64-of-username:token"}}}'