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]>
41 lines
2.8 KiB
YAML
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.
|