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
@@ -63,14 +63,41 @@ Files:
|
||||
- `platform/vpn-ingress/README.md`, `runbooks/vpn-remote-access.md` — step 2 rewritten.
|
||||
- `infra/vpn-ingress/new-peer.sh` — stale "step 1" pointer.
|
||||
|
||||
**A second finding, which changes the procedure independently of SOPS: `wg` and `sops` are not in
|
||||
the same place.** Measured: `wg` is on the **host** only (`/usr/bin/wg`, absent from the dev
|
||||
container); `sops` is in the **container** only (`/usr/local/bin/sops`, absent from the host). So
|
||||
`sops -d … | wg pubkey` cannot be run in one shell, and #55's step 2 — which assumed it could —
|
||||
would have failed. The runbook and the `.example` now split every command by where it runs, and
|
||||
verify the written key by comparing **sha256 digests** of the decrypted value and the operator's
|
||||
local copy, which never puts key material on a terminal. `qrencode` is on neither, so the QR
|
||||
instruction now says so and gives the install command.
|
||||
**A second finding: `wg` and `sops` were not in the same place — and the first fix for that was
|
||||
also wrong.** Measured: `wg` existed only on the host (`/usr/bin/wg`), `sops` only in the dev
|
||||
container, `qrencode` on neither. So `sops -d … | wg pubkey` could not run in one shell, and #55's
|
||||
step 2 — which assumed it could — would have failed.
|
||||
|
||||
My first fix was to **split every step by where it runs**, and to tell the operator to
|
||||
`dnf install qrencode` on their workstation. **Operator correction: "sop, wg, and any other tool
|
||||
requirement is supposed to be baked into the devcontainer."** That is CLAUDE.md §5 and the
|
||||
devcontainer README's "Don't install the toolchain on the host", and the split fix broke the
|
||||
invariant rather than honouring it — it also produced a procedure that could never be reproduced in
|
||||
CI, and which no one should have to follow.
|
||||
|
||||
**The real fix:** `wireguard-tools` and `qrencode` are now distro packages in
|
||||
`infra/devcontainer/Containerfile`, alongside ansible/openssh/diffutils. Both are in Fedora 44's
|
||||
repos (`wireguard-tools` 1.0.20260223 — the same upstream version as the container image this
|
||||
component runs — and `qrencode` 4.1.1), unpinned like every other dnf package there, because 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 operator's rule
|
||||
was the right one:
|
||||
|
||||
1. **Step 2 is now one pipeline in one shell:** `sops set … "\"$(wg genkey)\""` generates the key
|
||||
straight into the already-encrypted document. The plaintext never lands on disk, never reaches a
|
||||
terminal, and there is no staged copy to shred. The previous version needed a copy in
|
||||
`.cache/server.key`, a `git check-ignore` warning about the `*.key` suffix being the only thing
|
||||
protecting it, and a sha256 comparison to prove the right bytes arrived. All of that is gone.
|
||||
2. **`new-peer.sh` now derives the server's 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. It previously required the operator to keep the private key at a magic host path.
|
||||
3. **`.cache/` is now gitignored at the repo root** (mirroring `infra/ansible/.gitignore`), so the
|
||||
container has a persistent, non-committable scratch directory. `new-peer.sh` writes client
|
||||
profiles to `.cache/wg-peers/` by default — necessary because the repo bind-mount is the
|
||||
container's ONLY persistent read-write mount, so a profile written to `/tmp` in there is gone
|
||||
before it can be moved to a desktop. Its old blanket "refuse to write inside the checkout" guard
|
||||
is replaced by `git check-ignore`, which tests the actual property instead of a proxy.
|
||||
|
||||
**SENSITIVE PATHS — this PR touches `clusters/**` (two overlays) AND adds a `**/*.sops.yaml`, so
|
||||
`ci/approval-policy.sh` will BLOCK it.** It needs the operator's `/approve-override <reason>` as a
|
||||
@@ -91,7 +118,21 @@ PR **review** of type "Comment". I have deliberately not posted it (CLAUDE.md §
|
||||
- **`kubectl kustomize platform/vpn-ingress` builds** and now emits five objects — Namespace,
|
||||
ConfigMap, **Secret**, DaemonSet, NetworkPolicy — with `${WG_CLIENT_CIDR}` still an unexpanded
|
||||
token for Flux.
|
||||
- **`wg` / `sops` / `qrencode` locations**, measured on both host and container, as described above.
|
||||
- **The tool gap, measured on both host and container before the fix**: `wg` host-only, `sops`
|
||||
container-only, `qrencode` on neither.
|
||||
- **Both packages exist in the image's repos**, queried from inside the running container:
|
||||
`wireguard-tools` 1.0.20260223 (repo `updates`) and `qrencode` 4.1.1 (repo `fedora`).
|
||||
- **The image REBUILDS with them and both tools resolve inside it** —
|
||||
`./infra/devcontainer/dev.sh build` succeeded, and `wg --version` / `qrencode --version` /
|
||||
`sops --version` all answer in the rebuilt container. This is the claim the change rests on, so
|
||||
it was executed rather than reasoned.
|
||||
- **`new-peer.sh`'s new SOPS path fails correctly against the placeholder.** Run in the rebuilt
|
||||
container it refused with "could not derive the server public key … it still holds the REPLACE-ME
|
||||
placeholder", which is both the expected pre-key behaviour and a live test of the
|
||||
`sops -d | wg pubkey` pipeline the whole design now depends on.
|
||||
- **`.cache/` is ignored regardless of filename** now: `git check-ignore -v .cache/anything.conf`
|
||||
and `.cache/serverkey.txt` both match `.gitignore:46:.cache/`. Before this change only `*.key`
|
||||
matched, which is the fragility the previous runbook had to warn about.
|
||||
- **Toolbox gate** from an isolated worktree: **rc 3**, with `spec-immutability`, `decisions-log`,
|
||||
`lint`, `l0`, `render-golden`, `l1` and `approval-policy` all **pass**; rc 3 is the two
|
||||
environmental skips (`gitea-settings` — no local token; conformance L2/L3 — no cluster).
|
||||
|
||||
Reference in new issue
Block a user