fix(devcontainer): bake wg + qrencode into the image instead of splitting the VPN procedure across host and container
gate / conformance (pull_request) Has been skipped
gate / gate (pull_request) Successful in 1m53s

Operator correction: "sop, wg, and any other tool requirement is supposed to be
baked into the devcontainer."

My previous commit discovered that `wg` existed only on the host, `sops` only in
the container, and `qrencode` on neither -- and fixed it the wrong way, by
splitting every step of the key procedure across the boundary and telling the
operator to `dnf install qrencode` on their workstation. That breaks CLAUDE.md §5
and the devcontainer README's "Don't install the toolchain on the host", produces
a procedure that cannot be reproduced in CI, and is miserable to follow.

wireguard-tools + qrencode are now distro packages in the Containerfile. Both are
in Fedora 44's repos (wireguard-tools 1.0.20260223 -- the same upstream version as
the image this component runs -- and qrencode 4.1.1), unpinned like every other
dnf package there: the pinned thing is the Fedora base, and neither is a gate tool
whose version changes what green means.

Three things got SIMPLER rather than merely relocated, which is the tell that the
rule was right:

  * Step 2 is one pipeline in one shell -- `sops set ... "$(wg genkey)"` writes the
    key straight into the already-encrypted document. No staged copy in .cache, no
    `*.key`-suffix warning, no sha256 comparison to prove the right bytes landed.
  * new-peer.sh derives the server public key from wg-server-key.sops.yaml itself
    (`sops -d --extract | wg pubkey`), so there is no local key copy to keep in
    step with the encrypted one, and no magic host path it must live at.
  * `.cache/` is gitignored at the repo root (mirroring infra/ansible/.gitignore),
    giving the container a persistent non-committable scratch dir. new-peer.sh
    writes profiles to .cache/wg-peers/ -- necessary because the repo bind-mount is
    the container's only persistent read-write mount, so a profile in /tmp there is
    gone before it can reach a desktop. Its blanket "refuse to write inside the
    checkout" guard is now a `git check-ignore` test of the real property.

Verified by execution, not reasoning: the image rebuilds; wg/qrencode/sops all
resolve in it; `sops -d | wg pubkey` runs in one shell; new-peer.sh refuses the
REPLACE-ME placeholder with an actionable message; and a full run with a throwaway
server key renders a correct profile at 0600 under a 0700 gitignored dir, prints
the QR, and refuses a non-ignored destination inside the repo.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
jakman85andClaude Opus 5 committed 2026-10-06 00:48:32 -05:00
1 parent e35612dc7e
commit 8059297036
7 files changed
+212 -123

No files matched your search

@@ -22,52 +22,46 @@
# producing a server that silently cannot complete a handshake.
#
# ---------------------------------------------------------------------------------------------
# MIND WHERE EACH COMMAND RUNS. The two tools do not live in the same place (measured 2026-10-05):
# wg -> the HOST only (/usr/bin/wg; NOT in the dev container)
# sops -> the dev CONTAINER only (/usr/local/bin/sops; NOT on the host)
# So you cannot pipe `sops -d ... | wg pubkey` in one shell. The steps below are split accordingly,
# and they never put the key on a terminal or in a pipeline that crosses the boundary.
# EVERYTHING RUNS IN THE DEV CONTAINER. `wg`, `sops` and `qrencode` are all baked into the image
# (infra/devcontainer/Containerfile) because that image IS the toolchain — CLAUDE.md §5, and the
# devcontainer README's "Don't install the toolchain on the host". One shell, one toolchain, so
# `wg genkey | sops set` is just a pipeline.
# ---------------------------------------------------------------------------------------------
#
# HAND-OFF STEPS (operator — an agent must not mint a long-lived private key, CLAUDE.md §6).
# Full procedure with verification: runbooks/vpn-remote-access.md step 2.
#
# 1. HOST — generate it. This file is also what infra/vpn-ingress/new-peer.sh reads to derive the
# public half for each client profile, so keep it where the script's default expects it:
# umask 077
# mkdir -p ~/.config/wireguard-home && chmod 700 ~/.config/wireguard-home
# wg genkey > ~/.config/wireguard-home/server.key
# 1. Generate the key STRAIGHT INTO this file. The plaintext never lands on disk and never
# reaches a terminal; `sops set` edits the already-encrypted document in place:
# ./infra/devcontainer/dev.sh run bash -c '
# cd /work
# umask 077
# sops set platform/vpn-ingress/wg-server-key.sops.yaml \
# "[\"stringData\"][\"privatekey\"]" "\"$(wg genkey)\""
# '
# (Or edit it: `./infra/devcontainer/dev.sh run sops platform/vpn-ingress/wg-server-key.sops.yaml`.)
#
# 2. Put that value in your PASSWORD MANAGER. It cannot be regenerated without invalidating every
# client profile. (Velero backs this namespace up to R2 too, but the password-manager copy is
# the one that survives losing the cluster AND the backups.)
# Record the public half alongside it — safe to display, and useful when debugging a client:
# wg pubkey < ~/.config/wireguard-home/server.key
# 2. Record BOTH halves in your PASSWORD MANAGER. The encrypted file is in git and Velero captures
# this namespace to R2, so this is belt-and-braces — but it is the only copy that survives
# losing the cluster AND the backups, and the key cannot be regenerated without invalidating
# every client profile.
# # public half (safe to display; it is what every client profile carries)
# ./infra/devcontainer/dev.sh run bash -c 'cd /work && sops -d \
# --extract "[\"stringData\"][\"privatekey\"]" \
# platform/vpn-ingress/wg-server-key.sops.yaml | wg pubkey'
#
# 3. Stage it where the CONTAINER can read it, then write it into the encrypted file in place:
# mkdir -p .cache && cp ~/.config/wireguard-home/server.key .cache/server.key
# git check-ignore -v .cache/server.key # MUST print `.gitignore:11:*.key` — see below
# ./infra/devcontainer/dev.sh run bash -c \
# 'cd /work && sops set platform/vpn-ingress/wg-server-key.sops.yaml \
# "[\"stringData\"][\"privatekey\"]" "\"$(tr -d "\r\n" < /work/.cache/server.key)\""'
# shred -u .cache/server.key
# `sops set` edits the ENCRYPTED document directly — no plaintext copy of it is ever written.
# 3. Confirm the value is a real key without printing it — `wg pubkey` succeeds only on a valid
# 32-byte curve25519 private key, which is exactly the check the pod's init container runs:
# ./infra/devcontainer/dev.sh run bash -c 'cd /work && sops -d \
# --extract "[\"stringData\"][\"privatekey\"]" \
# platform/vpn-ingress/wg-server-key.sops.yaml | wg pubkey >/dev/null \
# && echo "valid WireGuard key"'
#
# KEEP THE `.key` SUFFIX on that staged copy. `.gitignore` line 11 is `*.key`, and it is the
# ONLY rule covering that path: `.cache/` itself is NOT ignored. A copy named `server.pem` or
# `privatekey` in the same directory is COMMITTABLE, and the gate's `no-plaintext-secrets` stage
# does not recognise a bare WireGuard key.
#
# 4. Verify you wrote the right bytes WITHOUT printing them — compare digests, not values:
# ./infra/devcontainer/dev.sh run bash -c \
# 'cd /work && sops -d --extract "[\"stringData\"][\"privatekey\"]" \
# platform/vpn-ingress/wg-server-key.sops.yaml | tr -d "\r\n" | sha256sum'
# tr -d '\r\n' < ~/.config/wireguard-home/server.key | sha256sum
# The two digests must match. (This is the only sanctioned decrypt of this file, and its output
# goes straight into sha256sum — never onto a terminal. CLAUDE.md §6.)
#
# 5. PR it. Nothing else to wire: the resource line and both overlay decryption patches already
# 4. PR it. Nothing else to wire: the resource line and both overlay decryption patches already
# exist. Flux rolls the DaemonSet and the pods leave CrashLoop.
#
# NOTE infra/vpn-ingress/new-peer.sh reads THIS FILE to derive each client profile's
# `[Peer] PublicKey`, so there is no separate copy of the key to keep in step with anything.
# =============================================================================
apiVersion: v1
kind: Secret