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]>
77 lines
4.7 KiB
Plaintext
77 lines
4.7 KiB
Plaintext
# =============================================================================
|
|
# TEMPLATE — NOT APPLIED, NOT ENCRYPTED (.yaml.example is ignored by kustomize and never
|
|
# auto-encrypted by SOPS). Reference scaffold + OPERATOR HAND-OFF.
|
|
#
|
|
# WHAT THIS IS: the WireGuard SERVER's long-term private key for the remote-access VPN
|
|
# (platform/vpn-ingress). It is the ONE secret in the whole component. The init container in
|
|
# daemonset.yaml reads it at `/run/wg-server-key/privatekey` and joins it to the PLAINTEXT peer
|
|
# register (peers.yaml) in a tmpfs, so the assembled wg0.conf never touches a node's disk.
|
|
#
|
|
# WHY IT IS IN GIT, SOPS-ENCRYPTED, AND NOT HAND-CREATED. R-SEC-01 / spec docs/07: secrets live in
|
|
# source, encrypted, and reach the cluster only through Flux. An out-of-band `kubectl create secret`
|
|
# would put the single credential every enrolled device depends on OUTSIDE the GitOps tree, where a
|
|
# cluster rebuild silently loses it — and losing it means RE-ENROLLING EVERY DEVICE, because each
|
|
# client profile pins this key's public half.
|
|
#
|
|
# *** THE REAL FILE IS COMMITTED WITH `privatekey: REPLACE-ME`, ALREADY ENCRYPTED. ***
|
|
# That is deliberate, so the wiring is complete and green before the key exists: the resource is
|
|
# already listed in kustomization.yaml and both overlays already carry the `vpn-ingress` decryption
|
|
# patch. Your job is ONLY to replace the value (step 3 below). Until you do, the DaemonSet
|
|
# CrashLoops with an explicit FATAL naming this file — the init container validates the value with
|
|
# `wg pubkey` before writing wg0.conf, so a placeholder fails CLOSED and LOUDLY rather than
|
|
# producing a server that silently cannot complete a handshake.
|
|
#
|
|
# ---------------------------------------------------------------------------------------------
|
|
# 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. 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. 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. 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"'
|
|
#
|
|
# 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
|
|
metadata:
|
|
name: wg-server-key
|
|
namespace: vpn-ingress
|
|
type: Opaque
|
|
stringData:
|
|
# The server's WireGuard private key: `wg genkey` output — 44 chars of base64 ending in '='.
|
|
# NOT a passphrase, and NOT the public key. The init container validates it with `wg pubkey`
|
|
# before writing wg0.conf, so a placeholder or a mangled value fails closed with a clear message.
|
|
privatekey: REPLACE-ME
|