# ============================================================================= # 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. # # --------------------------------------------------------------------------------------------- # 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. # --------------------------------------------------------------------------------------------- # # 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 # # 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 # # 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. # # 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 # exist. Flux rolls the DaemonSet and the pods leave CrashLoop. # ============================================================================= 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