# Cilium Helm values — rendered by the Ansible bootstrap (T-1.3). # PARITY CONTRACT (docs/02): the fields marked [parity] MUST equal the # platform/cilium HelmRelease values, or Flux and the bootstrap fight over the CNI. --- # [parity] k3s kube-proxy is disabled → Cilium replaces it (ADR-0001/0002). kubeProxyReplacement: true # [parity] = k3s_apiserver_endpoint, THE HA CONTROL-PLANE VIP (kube_vip_address) — dev # 192.168.1.49, prod 192.168.2.9. NOT k3s_api_host, which is the FOUNDER'S OWN node IP: # pointing the CNI's kube-proxy replacement at one machine on a 3-server HA cluster means # losing that machine stops every service/policy update reaching the datapath, silently # (the existing datapath keeps working from Cilium's cache, so nothing looks broken). That # was the live state of prod until 2026-09-06 — all three cilium-agents carried # KUBERNETES_SERVICE_HOST=192.168.2.10 while kube-vip held .9. See group_vars/all.yml. # # [parity] The Flux side is platform/cilium/helmrelease.yaml's k8sServiceHost: # "${K3S_API_HOST}", whose per-environment value comes from clusters//kustomization.yaml. # THAT VALUE MUST EQUAL k3s_apiserver_endpoint for the SAME environment. If it does not, # Flux and this bootstrap render different control-plane paths and fight over the CNI. # # THE VIP MUST BE UP BEFORE THIS ROLE RUNS. playbooks/site.yml imports kube-vip.yml between # k3s.yml and cilium.yml for exactly this reason, and roles/kube_vip blocks until the VIP # answers. Rendering this against a VIP that nothing announces yet reproduces the # 2026-09-01 shape: agents that cannot reach the API server on a cold cluster. k8sServiceHost: "{{ k3s_apiserver_endpoint }}" k8sServicePort: {{ k3s_api_port }} # Ingress via Gateway API — no Traefik (docs/09, ADR-0002). gatewayAPI: enabled: true # Egress-class steering (ADR-0002; VPN model ADR-0013, Phase 2/3). egressGateway: enabled: true # Egress Gateway REQUIRES masquerade (ipv4 + eBPF). eBPF masquerade is the correct # pairing with kubeProxyReplacement. [parity] must match platform/cilium HelmRelease. enableIPv4Masquerade: true bpf: masquerade: true # Flow observability / isolation audit (ADR-0002). # Roll the agents when cilium-config changes. Kept EQUAL to the Flux values # (platform/cilium/helmrelease.yaml), which carry the full rationale: the chart default of false # leaves the pod template untouched on a config change, and cilium-agent reads that ConfigMap only # at startup, so the change is silently inert until someone restarts the DaemonSet by hand. rollOutCiliumPods: true hubble: enabled: true relay: enabled: true ui: enabled: true # Static flow export -> cilium-agent stdout. Kept EQUAL to the Flux values # (platform/cilium/helmrelease.yaml), which carry the full rationale: it records the PRE-SNAT # client address for Plex's raw tcp/32400, which Plex itself cannot log because every # LoadBalancer Service here is externalTrafficPolicy: Cluster and the VIP lease holder SNATs the # client. Present here so a BOOTSTRAP render does not differ from what Flux reconciles -- a # mismatch would show up as a permanent Helm diff and an avoidable agent rollout. Scoped by PORT, # not namespace: the flow carrying the real client address does not match a namespace filter. export: static: enabled: true filePath: stdout allowList: - '{"destination_port":["32400"]}' - '{"source_port":["32400"]}' # Ingress VIP announcement on the NAT LAN. IP pool / CiliumL2AnnouncementPolicy # (from the reserved 10.73.0.240-.250 range) is authored by WS-NET in Phase 2. l2announcements: enabled: true # Point Cilium at the host's real cgroup2 root (/sys/fs/cgroup, mounted by systemd) instead of # auto-mounting. NOT /run/cilium/cgroupv2: nothing mounts cgroup2 there, so the agent mounted its # OWN namespaced cgroup and socket-LB covered only itself — host processes (the apiserver) lost # Service/LB translation. [parity] platform/cilium/helmrelease.yaml explains (2026-10-03). cgroup: autoMount: enabled: false hostRoot: /sys/fs/cgroup # Roll the cilium agent DaemonSet ONE node at a time. [parity] must match the # platform/cilium HelmRelease. The cilium 1.20.1 chart defaults this to 2, which on a # 3-node cluster lets one bad render take down two of three agents simultaneously — # exactly the 2026-09-01 incident, where k8sServiceHost: "" made the agent's `config` # init container fall back to the in-cluster ClusterIP 10.43.0.1:443. Only the Cilium # agent can translate that ClusterIP, and the agent in question was the one being # replaced: `dial tcp 10.43.0.1:443: i/o timeout`, deadlocked. # A CNI rollout has no healthy-peer fallback — the component being upgraded is the one # the upgrade needs to reach the API server. This bites hardest on a COLD BOOTSTRAP # (this role's job), where nothing healthy exists to recover from. With maxUnavailable: 1 # the DaemonSet will not proceed past an unready pod, bounding damage to a single node. # Cost: Cilium upgrades serialise and take longer; at 3 nodes that is an acceptable trade. updateStrategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 operator: replicas: {{ cilium_operator_replicas }}