Files
home-network-k3s-impl/decisions/2026-10-05-vpn-runbook-ansible-command.md
jakman85andClaude Opus 5 18f741d744
gate / gate (pull_request) Successful in 1m47s
gate / conformance (pull_request) Has been skipped
fix(runbooks): vpn-remote-access step 1 named a nonexistent script; step 2 cited the wrong gitignore rule
Both reported by the operator on first use of the runbook merged in #55.

Step 1 said `cd infra/ansible && ./site.sh --tags system --limit k3s_prod`.
There is no site.sh anywhere in this repo. It was wrong four ways at once:
this repo drives Ansible with `ansible-playbook <playbook>` and never a
wrapper (playbooks/site.yml is the full bring-up chain, not this); the module
work is in playbooks/node-baseline.yml; targeting here is `-e k3s_hosts=...`,
not `--limit`; and Ansible exists only in the pinned toolbox, with ansible.cfg
read only from infra/ansible. Replaced with the real command plus why
`-e k3s_hosts=k3s_prod` matters (the default group is decommissioned dev) and
why `--tags system` must never be dropped (an untagged run also executes the
`disks` tasks, which assert on prod's deliberate placeholder devices).

Step 2 told the operator to stage the server key at /work/.cache/server.key
"inside the repo's gitignored cache". .cache/ is NOT gitignored -- the file was
ignored only incidentally by `*.key` (.gitignore:11). So the path was safe for
a reason I had not identified, and renaming it (server.pem, wg-server) would
have left a committable WireGuard private key in the checkout, which
no-plaintext-secrets does not recognise. The step now names the real rule,
marks the suffix as the load-bearing part, and gives a check-ignore
verification to run before trusting it.

Docs only -- no manifest and no behaviour change. Touches no sensitive path.
Also records the post-merge measurements for #55, whose own entry predates the
merge: Flux applied ad3f56e by the real path, all 18 Kustomizations Ready,
hostNetwork confirmed (pod IPs are the node IPs), and the designed fail-closed
state (Init:0/1 on the missing wg-server-key Secret).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-10-05 23:08:19 -05:00

7.2 KiB

date, branch, title
date branch title
2026-10-05 fix/vpn-runbook-ansible-command vpn-remote-access runbook — step 1 named a script that does not exist, and step 2 cited the wrong reason the key is gitignored

What changed

Two defects in runbooks/vpn-remote-access.md (merged in #55), both authored by me, both reported by the operator the first time they tried to follow it.

1. Step 1 named a command that does not exist. It said:

cd infra/ansible && ./site.sh --tags system --limit k3s_prod

There is no site.sh anywhere in this repo (find . -name site.sh → nothing). The invocation was wrong in four separate ways at once, which is what makes it worth recording rather than just patching: wrong kind of thing (this repo drives Ansible with ansible-playbook <playbook>, never a wrapper script — playbooks/site.yml exists and is the full bring-up chain, not this); wrong playbook (the module work lives in playbooks/node-baseline.yml); wrong targeting flag (--limit instead of this repo's -e k3s_hosts=... convention, which infra/ansible/README.md "Targeting prod" documents for every playbook); and no toolbox wrapper, though Ansible exists only inside the pinned container and ansible.cfg is only read from infra/ansible.

Replaced with the real command, plus the three things about it that are load-bearing:

./infra/devcontainer/dev.sh run bash -c \
  'cd /work/infra/ansible && ansible-playbook playbooks/node-baseline.yml -e k3s_hosts=k3s_prod --tags system'
  • -e k3s_hosts=k3s_prod — the playbook defaults to k3s_servers, which is the dev group; dev is decommissioned, so an unqualified run does nothing useful.
  • --tags system, never untagged — the two kernel-module tasks carry that tag, and an untagged run would also hit the disks tasks, which assert on block-device variables prod deliberately leaves as placeholders (the playbook's own header warns about this). --tags system is broader than the modules alone (sysctls, iscsid, qemu-guest-agent, swap masking) but each is idempotent and already in its desired state on a live node.

2. Step 2 gave the wrong reason the server key is safe from git — and the right reason is fragile. It told the operator to stage the key at /work/.cache/server.key "inside the repo's gitignored cache". .cache/ is not gitignored: git check-ignore -v .cache/serverkey.txt matches nothing. The file was ignored only incidentally, by .gitignore line 11 *.key — so the path happened to be safe for a reason I had not identified, and any operator who renamed it (server.pem, wg-server, privatekey) would have had a committable WireGuard private key sitting in the checkout. The gate would not have caught it either; no-plaintext-secrets does not recognise a bare WireGuard key.

The step now states the real rule, marks the .key suffix as the thing doing the work, and gives the operator a verification they can run before trusting it (git check-ignore -v must print a rule, and git status --short must not list the file).

Nothing outside runbooks/vpn-remote-access.md changed; no manifest, no behaviour.

What I verified

  • site.sh does not exist: find . -name 'site.sh' -not -path './.git/*' → no output.
  • The real surface: infra/ansible/playbooks/ contains node-baseline.yml and site.yml (and 11 others); node-baseline.yml's own header documents both -e k3s_hosts=k3s_prod and the tag-scoping hazard. The -e <playbook>_hosts= convention is confirmed by infra/ansible/README.md §"Targeting prod" and by the existing runbooks/node-memory-swap.md, which invokes ansible-playbook playbooks/node-health.yml -e node_health_hosts=....
  • k3s_prod is a real inventory group: [k3s_prod:children] in inventory/hosts.ini, with [k3s_prod_primary]/[k3s_prod_secondary].
  • --tags system covers the kernel-module tasks: both "Persist kernel modules…" and "Load kernel modules now" carry tags: [system] in roles/node_baseline/tasks/main.yml.
  • The gitignore claim, measured both directions: git check-ignore -v .cache/server.key → .gitignore:11:*.key; git check-ignore -v .cache/serverkey.txt → no match. So the suffix is the only thing ignoring it.
  • Every path the component's docs reference resolves: all six relative markdown links in platform/vpn-ingress/README.md (checked from that file's own directory), and both Ansible playbook paths.
  • Toolbox gate from an isolated worktree: rc 3, with spec-immutability, decisions-log, lint, l0, render-golden, l1 and approval-policy all pass; the rc 3 is the two environmental skips (gitea-settings — no local token; conformance L2/L3 — no CLUSTER_AVAILABLE). Not green. Reported as rc 3, not as pass.
  • This branch touches NO sensitive path (runbooks/** and decisions/** are not in infra/gitea/approval-policy.json), so unlike #55 it needs no /approve-override.

What I did NOT verify

  • The corrected command has not been RUN. Ansible is an operator action, and this is the decisive gap: I have verified the playbook, the group, the tag and the flag all exist and are the documented convention, but not that the run succeeds end to end or that lsmod | grep '^wireguard' then reports the module. It is a better-evidenced command, not a proven one.
  • Whether --tags system is entirely side-effect-free on a live prod node. Each task it runs is idempotent by construction, but I did not dry-run it (--check) to confirm it reports zero changes beyond the module load.
  • The rest of the runbook's commands past step 2 are unchanged and still unexercised — in particular kubectl exec ds/wireguard -- wg show, which cannot be reached while the pods sit in Init waiting on the server key.
  • Whether any OTHER document I wrote in #55 contains an invented command. I re-checked every file path and markdown link in the component (all resolve) and every script the docs tell an operator to run, but "the referenced file exists" is weaker than "the command is correct" — which is exactly the gap that produced site.sh.

Post-merge state of #55, recorded here since its own entry predates the merge

Measured on live prod after #55 merged as ad3f56e:

  • Flux applied it by the real path: Kustomization vpn-ingress lastAppliedRevision: main@sha1:ad3f56e, equal to the GitRepository's artifact revision, Ready=True.
  • All 18 Flux Kustomizations Ready=True — the new tier disturbed nothing, and wait: false behaved as intended (vpn-ingress is Ready while its own pods are not, so nothing downstream stalled on it).
  • hostNetwork is confirmed in effect, not merely configured: the three pods report pod IPs 192.168.2.10/.11/.12 — the node addresses, one per node.
  • It fails closed exactly as designed, with a legible cause: all three pods Init:0/1, MountVolume.SetUp failed for volume "wg-server-key" : secret "wg-server-key" not found, and no Secret in the namespace. The #55 entry's "the DaemonSet has never been reconciled" is superseded by this paragraph; its remaining unverified claims (the tunnel carrying a packet, the iptables/nftables backend question) still stand.