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]>
60 lines
3.3 KiB
YAML
60 lines
3.3 KiB
YAML
---
|
|
# Flux Kustomization: WireGuard remote-access VPN — reconciles ./platform/vpn-ingress.
|
|
#
|
|
# POST-PLATFORM TIER (dependsOn platform-config), NON-GATING (wait:false). Not a line in
|
|
# platform/kustomization.yaml, for the standard reason plus one specific to this component:
|
|
#
|
|
# * STANDARD: a leaf must never sit in the gating `platform` tier. While `platform` is not-Ready
|
|
# NOTHING downstream reconciles — the incident behind the headlamp/oauth2-proxy/homepage moves
|
|
# (DECISIONS-LOG: "Headlamp gating stall").
|
|
# * SPECIFIC, AND THE REASON THIS IS NOT A JUDGEMENT CALL: this DaemonSet cannot start until the
|
|
# operator replaces the `REPLACE-ME` placeholder in platform/vpn-ingress/wg-server-key.sops.yaml
|
|
# with a real key (runbooks/vpn-remote-access.md step 2). That is the correct fail-closed state — an unenrolled VPN must not come up — but in
|
|
# the gating tier it would have been a self-inflicted platform-wide outage ON MERGE, with the
|
|
# cause (a missing manual step) invisible from any Flux condition.
|
|
#
|
|
# WHY dependsOn platform-config AND NOT cilium. This component needs no Gateway, no TLS cert, no
|
|
# Keycloak and no LB-IPAM pool — it has no HTTPRoute and no LoadBalancer Service, and hostNetwork
|
|
# means it does not even need a Cilium endpoint. It could legally reconcile much earlier. It is
|
|
# pinned behind platform-config anyway so that the ONE thing remote clients actually consume
|
|
# through it — split-horizon DNS and the shared Gateway — exists by the time the tunnel does.
|
|
# A tunnel that comes up before the things on the far side of it is a support call, not a feature.
|
|
#
|
|
# wait:false: nothing in the graph dependsOn this, and its pods are legitimately not-Ready whenever
|
|
# the server key is absent or the `wireguard` kernel module has not been loaded on a node. Neither
|
|
# condition should be able to gate anything else, even if a future resource is mistakenly pointed
|
|
# at it.
|
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
|
kind: Kustomization
|
|
metadata:
|
|
name: vpn-ingress
|
|
namespace: flux-system
|
|
spec:
|
|
interval: 10m
|
|
retryInterval: 2m
|
|
timeout: 5m
|
|
prune: true
|
|
wait: false
|
|
dependsOn:
|
|
- name: platform-config
|
|
sourceRef:
|
|
kind: GitRepository
|
|
name: flux-system
|
|
path: ./platform/vpn-ingress
|
|
# REQUIRED, not boilerplate: platform/vpn-ingress/daemonset.yaml renders ${WG_CLIENT_CIDR} into
|
|
# the config-assembly script. Without this block that token becomes the EMPTY STRING and the
|
|
# Kustomization still reports Ready (Flux's substitution fails OPEN — ci:
|
|
# flux-postbuild-substitution), leaving an initContainer that derives a server address of ".1/"
|
|
# and a MASQUERADE rule with no source. Both fail loudly at pod start, but the Kustomization
|
|
# would look healthy.
|
|
postBuild:
|
|
substituteFrom:
|
|
- kind: ConfigMap
|
|
name: cluster-vars
|
|
# SOPS decryption is layered per-environment by the overlay (clusters/dev and clusters/prod each
|
|
# patch THIS Kustomization by name), identical to headlamp/crafty/velero — this path carries
|
|
# platform/vpn-ingress/wg-server-key.sops.yaml, the WireGuard server private key. The `sops-age`
|
|
# Secret is created out of band and referenced by NAME only. Without the overlay patch the Secret
|
|
# applies cleanly full of ENC[...] and only the tunnel is broken, so the patch is as load-bearing
|
|
# as the encrypted file itself.
|