Cilium IPAM CIDR mismatch and Longhorn recovery¶
Cilium used an independent cluster-pool while K3s assigned different PodCIDRs to Kubernetes Nodes. The cluster initially remained functional, but the overlapping address plan created routing risk and left existing Pod sandboxes on the legacy range during migration. Rolling node restarts then exposed Longhorn processes still referencing an unreachable legacy Instance Manager address.
Symptoms¶
- Kubernetes Nodes and CiliumNode resources reported different per-node PodCIDRs;
- newly created Pods used the Cilium-managed range instead of
Node.spec.podCIDR; - the configured Cilium pool covered unrelated private networks used elsewhere in the homelab;
- after changing IPAM, existing Pods retained legacy addresses;
- after restarting one node, several Longhorn volumes became
detaching/faulted; - Kubernetes VolumeAttachment objects still requested attachment to the workload node;
- Longhorn replicas on another node remained reported as
runningthrough an unreachable legacy Instance Manager address; - affected workloads remained in init or container creation states while waiting for storage.
Component boundaries¶
The cluster intentionally keeps kube-proxy enabled:
The responsibilities are therefore separate:
| Component | Responsibility |
|---|---|
| CoreDNS | internal name resolution and forwarding external queries |
| kube-proxy | Service semantics and iptables DNAT from ClusterIP to endpoints |
| Cilium | Pod networking, IPAM, policy and VXLAN transport between nodes |
| EndpointSlice | current ready backends selected for a Service |
Cilium service-map entries are useful diagnostics, but they do not prove kube-proxy replacement is active. The active KUBE-SERVICES, KUBE-SVC-* and KUBE-SEP-* NAT chains confirmed that kube-proxy implemented ClusterIP translation.
Diagnosis¶
1. Compare Kubernetes and Cilium allocation state¶
kubectl get nodes \
-o custom-columns='NAME:.metadata.name,POD_CIDR:.spec.podCIDR,POD_CIDRS:.spec.podCIDRs'
kubectl get ciliumnodes.cilium.io \
-o jsonpath='{range .items[*]}{.metadata.name}: {.spec.ipam.podCIDRs[*]}{"\n"}{end}'
The failure condition is a different per-node CIDR reported by the two APIs.
Inspect active Cilium configuration:
kubectl get configmap cilium-config \
--namespace kube-system \
-o jsonpath='ipam={.data.ipam}{"\n"}kpr={.data.kube-proxy-replacement}{"\n"}routing={.data.routing-mode}{"\n"}'
2. Verify the service dataplane¶
Inspect kube-proxy NAT rules for a known Service:
The rules are programmed proactively from Service and EndpointSlice state. They are not created by an individual DNS lookup or application request.
3. Identify legacy Pod sandboxes¶
After the IPAM change, list active Pods still using the old range:
kubectl get pods --all-namespaces --output json |
jq -r '
.items[]
| select((.status.podIP // "") | startswith("<LEGACY_PREFIX>"))
| [.metadata.namespace, .metadata.name, .status.podIP, .spec.nodeName]
| @tsv
'
Changing IPAM affects new network sandboxes only. Existing Pods keep their addresses until recreated.
4. Inspect faulted Longhorn volumes before mutation¶
kubectl get volumes.longhorn.io \
--namespace longhorn-system \
-o custom-columns='NAME:.metadata.name,STATE:.status.state,ROBUSTNESS:.status.robustness,NODE:.status.currentNodeID'
For each affected volume, inspect replica placement and failure timestamps:
kubectl get replicas.longhorn.io \
--namespace longhorn-system \
--output json |
jq -r '
.items[]
| [
.spec.volumeName,
.metadata.name,
.spec.nodeID,
.spec.desireState,
.status.currentState,
(.spec.failedAt // "-"),
(.status.instanceManagerName // "-"),
(.status.ip // "-")
]
| @tsv
'
Do not delete VolumeAttachment or replica resources at this stage. First identify at least one recoverable replica and verify that its data directory exists on disk.
Root cause¶
The Cilium chart used its default cluster-pool IPAM mode. K3s simultaneously populated Node.spec.podCIDR from its own cluster CIDR. Cilium therefore allocated Pod addresses independently instead of consuming the Kubernetes-assigned ranges.
During migration, restarted Cilium agents began allocating from Node.spec.podCIDR, while existing Pods and Longhorn Instance Managers retained legacy addresses. A node restart killed volume engines and marked some replicas failed. The only replicas not marked failed were still reported through a legacy Instance Manager that was no longer reachable from the migrated network.
Longhorn could not complete detach because replica state remained inconsistent:
desired state: stopped
reported state: running
instance manager: unreachable legacy Pod IP
engine destination: empty address and port
Corrective action¶
1. Align Cilium with Kubernetes PodCIDRs¶
Configure the Flux-managed Cilium HelmRelease:
Take an embedded-etcd snapshot and export the relevant Kubernetes, Helm and Cilium state before deployment.
Apply the change through the resource owner. A direct Helm upgrade against a Flux-managed release can conflict with fields owned by helm-controller, especially with Server-Side Apply. Suspend the necessary reconciliation layers, update the HelmRelease source of truth, and let the Helm controller perform the upgrade.
Restart Cilium agents after the ConfigMap changes. A ConfigMap data update does not necessarily change the DaemonSet Pod template and therefore may not trigger an automatic rollout.
2. Validate new allocation before recycling existing workloads¶
Create one disposable Pod on each node and verify:
- its address belongs to that node's
Node.spec.podCIDR; - cluster DNS resolves through the stable
kube-dnsClusterIP; - cross-node Pod communication works through VXLAN;
- the Kubernetes API remains reachable through an independent management path.
3. Recycle nodes sequentially¶
Restart only one node at a time. Before continuing, require:
- the node is
Ready; - its Cilium agent reports the expected IPAM range;
- routes contain only the intended PodCIDRs;
- Longhorn volumes have returned to
healthy.
4. Recover an unreachable Longhorn Instance Manager¶
Before deleting any process Pod:
- confirm
auto-salvage=true; - confirm replica data directories exist on the affected node;
- identify every replica and engine handled by the Instance Manager;
- require affected replicas to have
desiredState=stopped; - require that the Instance Manager handles no active volume engine.
Delete only the stale Instance Manager Pod. If the node-local Longhorn manager also retains a legacy address and fails readiness, recreate only that DaemonSet Pod. The controllers then recreate the Instance Manager using the aligned PodCIDR and reconcile the stale replica state.
Do not manually create an Instance Manager Pod. It is owned by the Longhorn InstanceManager resource.
Validation¶
Validate Cilium runtime state on every node:
kubectl exec \
--namespace kube-system \
<CILIUM_POD> \
--container cilium-agent \
-- cilium-dbg status --verbose
Validate Node and CiliumNode CIDRs:
kubectl get nodes \
-o custom-columns='NAME:.metadata.name,POD_CIDR:.spec.podCIDR'
kubectl get ciliumnodes.cilium.io \
-o jsonpath='{range .items[*]}{.metadata.name}: {.spec.ipam.podCIDRs[*]}{"\n"}{end}'
Validate Longhorn:
kubectl get volumes.longhorn.io \
--namespace longhorn-system \
-o custom-columns='NAME:.metadata.name,STATE:.status.state,ROBUSTNESS:.status.robustness,NODE:.status.currentNodeID'
The verified final state was:
- all Kubernetes Nodes were
Readyand schedulable; - Cilium used Kubernetes IPAM and consumed the Node PodCIDRs;
- no active Pod retained an address from the legacy pool;
- CoreDNS and Service resolution worked from all nodes;
- every Longhorn volume was
attached/healthy; - all Flux Kustomizations converged on the merged
mainrevision; - the Cilium HelmRelease and active ConfigMap matched the Git declaration.
Longhorn temporarily reported attached/degraded after a node restart. This meant the volume remained available with reduced redundancy. After the configured replica replenishment delay, replacement replicas were built and the volumes returned to healthy.
Prevention¶
- define one authoritative Pod CIDR allocation model;
- compare
Node.spec.podCIDRand CiliumNode allocation after CNI installation; - avoid broad Pod pools that overlap other private networks;
- test new IP allocation with disposable Pods before recycling existing workloads;
- restart nodes sequentially and wait for storage redundancy before continuing;
- preserve an API and SSH management path independent of the Pod network;
- inspect replica state and on-disk data before forcing salvage or deleting attachments;
- operate Flux-managed Helm releases through their HelmRelease source of truth;
- merge the desired state into the branch observed by Flux before resuming reconciliation.