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]>
119 lines
6.8 KiB
Plaintext
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"}}}'
|