fix(devcontainer): bake wg + qrencode into the image instead of splitting the VPN procedure across host and container
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:
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
|
||||
|
||||
Reference in new issue
Block a user