Files
home-network-k3s-impl/platform/vpn-ingress/kustomization.yaml
T
jakman85andClaude Opus 5 e35612dc7e
gate / gate (pull_request) Failing after 1m51s
gate / conformance (pull_request) Has been skipped
fix(vpn-ingress): keep the server key in source through SOPS, not as an out-of-band Secret
Operator correction: secrets are kept in source through sops. #55 shipped the
WireGuard server private key as a Secret to `kubectl create` by hand. Two real
costs, not just a convention break:

  * R-SEC-01 / spec docs/07 -- secrets live in source, SOPS-encrypted, and
    reach the cluster only through Flux.
  * It broke DR for the one credential that cannot be re-derived. A rebuild
    restores everything in git; it does not restore a hand-made Secret, and
    losing this key means re-enrolling EVERY device, because each client
    profile pins its public half. #55 papered over that with "keep a copy in
    your password manager".

(Note secret-provenance did NOT forbid the original shape -- it scopes to
app-namespace=true and vpn-ingress is deliberately a platform namespace. The
rule broken was the written requirement, not an enforced control.)

An agent still must not mint a long-lived private key (CLAUDE.md §6), so the
file is committed ALREADY ENCRYPTED with `privatekey: REPLACE-ME`. That makes
the wiring complete and green from the first commit -- resource line plus both
overlay decryption patches -- leaving the operator exactly one act: replace the
value. A commented-out resource to uncomment later is the "in git, no generator
entry, reviewed, merged, still broken" shape this repo keeps hitting.

The init guard is now a validation rather than an emptiness check: the value is
piped through `wg pubkey`, which accepts only a real curve25519 private key, and
the pod names which case it hit -- placeholder, empty, mangled, or (the silent
one) still `ENC[` ciphertext because an overlay is missing its decryption patch,
which otherwise applies cleanly with every Flux condition green.

Second, independent finding that also fixes #55's step 2: `wg` is on the HOST
only and `sops` is in the CONTAINER only, so `sops -d | wg pubkey` cannot run in
one shell -- the command #55 documented could not have worked. Every step is now
split by where it runs, and the written key is verified by comparing sha256
digests rather than printing anything. `qrencode` is on neither, so the QR step
says so and gives the install command.

Touches clusters/** and adds a **/*.sops.yaml, so approval-policy will block
pending the operator's /approve-override. Deliberately not posted by me.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-10-05 23:55:29 -05:00

41 lines
2.8 KiB
YAML

---
# WireGuard remote-access VPN (north-south INGRESS). Reconciled by its OWN non-gating post-platform
# Flux Kustomization (clusters/base/vpn-ingress.yaml, dependsOn platform-config, wait:false) — the
# same placement as crafty/headlamp/homepage/runbooks, for the same reason: a leaf whose pod can be
# legitimately unhealthy must never sit in the gating `platform` tier, where one component's
# unhealth stalls platform-config and every apps wave behind it (DECISIONS-LOG: "Headlamp gating
# stall"). That rationale is unusually concrete here: until the operator replaces the `REPLACE-ME`
# placeholder in wg-server-key.sops.yaml (runbooks/vpn-remote-access.md step 2) this DaemonSet
# CANNOT start, so putting it in the gating tier would have taken the whole platform down on merge.
#
# NOT in platform/kustomization.yaml — see clusters/base/vpn-ingress.yaml.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
# The peer register: which devices may dial in. PLAINTEXT and reviewable on purpose (see the file).
- peers.yaml
# The listener itself: hostNetwork DaemonSet, udp/51820 on every node.
- daemonset.yaml
# Namespace default-deny baseline. NOTE: does NOT constrain the hostNetwork DaemonSet — the file
# says so at length rather than implying a protection it cannot provide.
- networkpolicy.yaml # The server's WireGuard private key — the ONE secret here. SOPS-encrypted (R-SEC-01, spec
# docs/07), decrypted per-env by the `vpn-ingress` Kustomization, referenced by name only from
# daemonset.yaml (R-IDP-04). Its plaintext `.example` sibling carries the operator hand-off --
# deliberately NOT named here: conformance/tests/l0-static/no-plaintext-secrets fails any
# kustomization that mentions an unencrypted template, because naming one is how a plaintext
# Secret gets applied by accident. (It matches comments too. That is correct, not overzealous.)
#
# SHIPS WITH A `REPLACE-ME` PLACEHOLDER, already encrypted. An agent must not
# mint a long-lived private key (CLAUDE.md §6), and the init container validates the value with
# `wg pubkey`, so the unfilled state is a loud CrashLoop naming this file, never a server that
# quietly cannot handshake.
- wg-server-key.sops.yaml
#
# THIS COMPONENT CARRIES AN ENCRYPTED SECRET, so BOTH overlays must patch the `vpn-ingress`
# Kustomization with a `decryption:` stanza — clusters/dev and clusters/prod both do, added by the
# same change as this file. That patch is as load-bearing as the file itself: without it kustomize
# hands Flux the raw ciphertext, the Secret APPLIES CLEANLY full of ENC[...], and nothing in any
# Flux condition looks wrong. The init container's `wg pubkey` guard exists partly to turn exactly
# that into a named error rather than a silently non-working tunnel.