The operator enrolled a phone, merged the peer, and `wg show` listed none. Measured on prod 2026-10-06 with the peer already on main: live wg-peers ConfigMap, active AllowedIPs .. 0 DaemonSet volume reference ................. configMap.name: wg-peers (FIXED) DaemonSet generation/observedGeneration .... 1 / 1 (never rolled) init container, all three pods ............. "0 peer(s)" Part of it was just the 10m reconcile interval. The defect is the fixed name: Flux updates a fixed-name ConfigMap IN PLACE, so the pod template is byte-identical, so no rollout is triggered -- and the init container assembles wg0.conf ONCE at pod start into a tmpfs, so the running tunnel keeps the old peer list indefinitely. Adding or revoking a peer had no effect until something unrelated restarted the pods. For a REVOCATION that is a security defect, not an inconvenience: delete a stanza, see a green PR, device keeps working. The runbook and README both claimed "PR -> merge -> Flux rolls the DaemonSet". Never true. And this repo already documents the fix, in a file I had read -- runbooks/kustomization.yaml: "The generated name carries kustomize's content hash, so editing any runbook rolls the server automatically and a stale projection is impossible." So: peers.yaml (inline ConfigMap) -> peers.conf (plain file) + a configMapGenerator. The operator's enrolled peer is carried across verbatim. Every line is WireGuard config and `#` is a legal comment there, so the documentation now lives inside the file that is actually mounted. `namespace: vpn-ingress` on the kustomization is REQUIRED, not tidiness: a generated ConfigMap inherits no namespace, everything else here declares vpn-ingress, and kustomize's nameReference will not rewrite across a namespace boundary -- without it the build mounts the unhashed name and the pods wedge on a missing volume. Caught only by building the output and reading the volumes. new-peer.sh: PEERS_FILE repointed, and a missing register is now FATAL rather than skipped -- the old `[ -r "$PEERS_FILE" ] && grep ...` guard meant that after the rename the only symptom would have been duplicate detection quietly never firing again. Stale peers.yaml references swept from six files including the script's own user-facing output. Verified: the DaemonSet now mounts wg-peers-m9kcckcb5t; the hash tracks content (edit -> dgg7ht9c74 -> restore -> m9kcckcb5t); the enrolled peer is byte-identical to main's; and `new-peer.sh dupe 2` is now correctly refused by the operator's real allocation. No sensitive paths; merges on a green gate. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
57 lines
3.0 KiB
YAML
57 lines
3.0 KiB
YAML
---
|
|
# =================================================================================================
|
|
# READ THIS BEFORE TRUSTING THIS FILE: it does NOT constrain the WireGuard listener.
|
|
# =================================================================================================
|
|
# Every other platform namespace in this repo (crafty, headlamp, homepage, runbooks) ships an
|
|
# explicit default-deny baseline as defense in depth, because Kyverno's generator only covers
|
|
# `app-namespace=true` namespaces. This file keeps that convention — but it would be dishonest to
|
|
# leave it at that, because the convention does not actually bite here:
|
|
#
|
|
# THE `wireguard` DaemonSet RUNS WITH hostNetwork: true, SO IT HAS NO CILIUM ENDPOINT, AND NO
|
|
# NetworkPolicy OR CiliumNetworkPolicy APPLIES TO IT — NEITHER THIS ONE NOR ANY OTHER.
|
|
#
|
|
# That is the documented reason restrict-host-namespaces forbids hostNetwork in app namespaces at
|
|
# all ("a pod on hostNetwork bypasses the CNI (Cilium) datapath and therefore ESCAPES the
|
|
# default-deny NetworkPolicy fabric -- it is the most direct way to defeat R-NET-01"). Here that
|
|
# escape is the deliberate, required design (daemonset.yaml explains why: without it, remote
|
|
# clients carry a pod identity and are 403'd by every Gateway hostname). A CiliumNetworkPolicy
|
|
# admitting `world` to udp/51820 would therefore be theatre: it would read as the control that
|
|
# protects this port, and it would be enforcing nothing. So there isn't one.
|
|
#
|
|
# WHAT ACTUALLY PROTECTS THE EXPOSED PORT, then, since the CNI does not:
|
|
# 1. WireGuard's cryptographic peer authentication. A packet whose key is not in peers.conf gets
|
|
# NO REPLY — see README "Why this is safe to expose". This is the real control.
|
|
# 2. The SOHO router's forward: only udp/51820 is forwarded inbound, to one node.
|
|
# 3. The blast radius once authenticated is deliberately the whole LAN — that IS the requirement
|
|
# ("clients should reach the entire local network"). README "What a client can reach" states
|
|
# the consequence in full, including reach to the nodes' own control plane, and gives the
|
|
# narrowing recipe for an operator who wants less than that.
|
|
#
|
|
# WHAT THIS FILE IS STILL FOR: it is the namespace's standing posture for any FUTURE pod-networked
|
|
# workload added here (a metrics exporter is the likely one — README "Not built"). Without it, such
|
|
# a pod would be born unrestricted, because Kyverno will not generate a baseline in a namespace
|
|
# that is not labelled `app-namespace=true`, and this one deliberately is not.
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: default-deny
|
|
namespace: vpn-ingress
|
|
labels:
|
|
homelab.keithjackson.dev/baseline: default-deny
|
|
spec:
|
|
podSelector: {}
|
|
policyTypes:
|
|
- Ingress
|
|
- Egress
|
|
ingress: []
|
|
egress:
|
|
- to:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
ports:
|
|
- protocol: UDP
|
|
port: 53
|
|
- protocol: TCP
|
|
port: 53
|