Files
homelab/kubernetes/PROPOSAL-cilium-single-owner.md
claude-bot 1f18599b4c
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
fix(talos): move cni: none and proxy: disabled into cluster.yaml
Both blocking findings from the review on #709, and both were real.

The deleted cilium-cni.yaml was the only place in the repo that set
cluster.network.cni.name: none and cluster.proxy.disabled: true. They sat in its
second document next to the inline manifest, so removing the file removed them
too — a fresh `talosctl gen config` from this repo's patches would have produced
a config with Talos's default CNI and kube-proxy running, against a Cilium
release configured with kubeProxyReplacement: true. Two runbooks meanwhile
asserted that cluster.yaml supplied both, which it did not.

RUNBOOK-baremetal-conversion.md also still passed `@<cilium additive half>` in
its gen-config example, three lines above prose saying there is no CNI patch any
more. The equivalent block in the cold-rebuild runbook had been fixed; this one
was missed.

Verified the way it should have been in the first place — by running the
runbook's own command rather than inspecting the live cluster:

    talosctl gen config homelab https://k8s.renner.uno:6443 \
      --output-types controlplane \
      --config-patch @talos/patches/cluster.yaml \
      --config-patch @talos/patches/registry-mirror.yaml \
      --config-patch @talos/patches/k8s-1.yaml

    -> cni=none  proxy.disabled=true
    -> 0 cilium manifests

The live cluster could never have caught this. It has carried both settings
since it was built, and the node configs were patched by hand rather than
regenerated, so nothing in the verification touched the path a rebuild actually
uses.
2026-08-28 16:21:18 +02:00

4.7 KiB

Cilium: one owner

The rule

Cilium is owned by Flux, through infrastructure/cilium/helmrelease.yaml. Talos provides the conditions for a CNI and nothing else. Never both.

Talos-only would also be a valid answer — it is the simpler one, and it keeps a cold rebuild self-forming. It is rejected here only because it costs Flux's reconciliation, image automation and value ergonomics for the one component most likely to need a careful, reviewed change.

Where we are

Both own it, which is the state this document exists to end.

  • 22 Cilium objects are listed in the talos-bootstrap-manifests-inventory ConfigMap: both DaemonSets, the operator Deployment, both ConfigMaps, the ServiceAccounts, the ClusterRoles/Bindings and the cilium-secrets namespace. That is the whole installation, not a fragment.
  • The same objects carry meta.helm.sh/release-name: cilium.
  • cilium-config has three field managers: helm-controller, talos and kubectl-patch.

The consequences are not theoretical. The seed sat at Cilium 1.19.2 for twelve days while the chart was on 1.20.1, and upgrade-k8s --dry-run shows that a Kubernetes upgrade in that state downgrades the CNI and switches off socket load balancing. Separately, enable-bgp-control-plane and the two native-routing CIDRs exist in no file at all — applied by hand, so a cold rebuild would have come up with BGP off while the peering CRDs in git made it look configured.

Nothing fails when these drift. That is the whole problem.

Target state

Talos keeps exactly this, in talos/patches/cluster.yaml, and gains nothing else:

cluster:
  network:
    cni:
      name: none
  proxy:
    disabled: true

No inlineManifests entry for cilium. talos/patches/cilium-cni.yaml is deleted.

Flux owns everything else. helmrelease.yaml is the single source for every Cilium setting, including the three that were hand-patched.

A cold rebuild installs Cilium once, by hand, between talosctl bootstrap and the Flux phase, from the same HelmRelease values. This is the cost of the decision and it is deliberate: the cluster no longer forms itself from the machine config alone. Given bootstrap.sh talos is already dead and Step 2 of RUNBOOK-cold-rebuild.md is already a manual procedure, the marginal cost is one documented command.

Transition

The order matters, because Talos prunes what its inventory used to list and no longer does — removing the manifest naively deletes the CNI.

  1. Track the untracked settings first. bgpControlPlane.enabled, ipv4NativeRoutingCIDR, ipv6NativeRoutingCIDR into the HelmRelease. Note this is not a pure change of ownership: enabling bgpControlPlane through the chart also emits five companion keys the hand-patch never set, so Cilium has been running on its own defaults for router-id allocation. Verify the BGP sessions after Flux applies it.

  2. Remove cilium from cluster.inlineManifests. This has no immediate effect. Evidence: Talos was upgraded to 1.13.9 four days ago and Cilium stayed at 1.20.1 — an OS upgrade does not reconcile bootstrap manifests. Only upgrade-k8s does.

  3. Prove the prune before running it. upgrade-k8s --dry-run --manifests-no-prune and confirm the 22 objects are not listed for deletion. NoPrune is honoured by the dry run as well as the apply (pkg/cluster/kubernetes/talos_managed.go), so this is a real test and not a reading of the flag description.

  4. Run upgrade-k8s --manifests-no-prune. The inventory is rewritten without Cilium and the objects survive. Helm is then sole owner. This is the same run that takes Kubernetes 1.35.2 → 1.36.3.

  5. Verify the inventory lists no Cilium, the agents did not restart, and the BGP sessions are still established.

  6. Delete talos/patches/cilium-cni.yaml and add the install step to RUNBOOK-cold-rebuild.md.

Not part of this, and not a defect either

The cluster runs routingMode: tunnel (the chart default, set nowhere) while also advertising pod CIDRs over BGP. That is not one layer too many — the two carry different directions:

  • VXLAN carries pod-to-pod between nodes, directly, node to node.
  • BGP gives the UDM routes into the pod CIDRs so traffic from the LAN can reach pods. That path goes through the router by definition.

Dropping the encapsulation in favour of native routing would not simplify anything; it would push inter-node pod traffic onto the underlay path and through the UDM, replacing a direct node-to-node hop with a router hop.

routingMode being unset is still worth knowing — the current behaviour is a default nobody chose rather than a decision anyone recorded — but the default is the right one here, and this document is not the place to revisit it.