Quand Cilium envoie le VXLAN sur la mauvaise interface : retour d’expérience sur un cluster RKE2 multi-NIC
Dans ce retour d’expérience, nous revenons sur un incident discret mais bloquant : dans un cluster RKE2 multi-NIC, le trafic VXLAN passait par le réseau d’administration au lieu du réseau applicatif, simplement parce que le nœud annonçait la mauvaise `InternalIP`.
Dans un cluster RKE2 géré par Rancher, avec une patte d’administration, une patte applicative et une patte de stockage, la promesse semble simple : chaque type de flux doit rester sur son propre réseau. Le control plane doit rester sur l’administration. Le stockage doit sortir sur sa patte dédiée. Et le trafic inter-workers doit passer par l’interface applicative.
Ce découpage est propre sur le papier. En pratique, il suffit qu’un seul mécanisme réseau reprenne la mauvaise adresse du nœud pour décaler tout le dataplane. C’est ce qui s’est produit ici : les workers remontaient en Ready, kubectl get nodes -o wide ne montrait rien d’alarmant, cilium status restait propre, et les workloads se déployaient. Pourtant, dès qu’un pod sur un worker devait joindre un pod sur un autre worker, le trafic échouait. Le VXLAN partait sur la mauvaise interface.
L’architecture minimale à garder en tête
La plateforme reposait sur deux clusters, avec deux rôles différents :
- un cluster de management, porté par Rancher, chargé d’administrer la plateforme ;
- un user cluster RKE2, chargé d’héberger les workloads applicatifs.
Dans le user cluster, les rôles n’étaient pas symétriques :
- les masters portaient le control plane RKE2 et les composants système ;
- les workers portaient les workloads et le dataplane applicatif.
Sur les workers, trois interfaces logiques coexistaient :
| Interface | Rôle | Ce qu’elle transporte |
|---|---|---|
admin | administration | enrôlement, API Kubernetes, flux de gestion |
appli | applicatif | trafic inter-workers, dataplane Cilium, exposition applicative |
storage | stockage | NFS et volumes persistants |
Pour que cette architecture tienne, il fallait donc une séparation nette :
- le control plane devait rester joignable via le réseau d’administration ;
- le trafic pod-to-pod entre workers devait passer par la patte applicative ;
- le stockage devait sortir sur sa propre interface ;
- les policies de sécurité devaient pouvoir bloquer le réseau d’administration sans casser les flux applicatifs légitimes.
Vue technique cible : Rancher sur K3s, RKE2 multi-NIC, bus séparés
Rancher reste sur K3s côté management. Dans le user cluster RKE2, le control plane reste sur admin, le dataplane Cilium et VXLAN passe sur appli, et le stockage suit storage / NFS.
Quelques notions à poser avant le cas réel
InternalIP, ExternalIP et node-ip
Dans Kubernetes, un objet Node expose plusieurs adresses dans son statut, dont InternalIP et parfois ExternalIP. La documentation Kubernetes sur les nœuds et la référence Node Status décrivent ce modèle. Côté kubelet, l’adresse publiée peut être influencée par --node-ip, documenté dans la référence kubelet.
Dans RKE2, ce réglage se fait généralement via le fichier de configuration de l’agent. La référence de configuration agent RKE2 documente node-ip, node-external-ip et lb-server-port.
# /etc/rancher/rke2/config.yaml.d/99-nodeip-app.yaml
node-ip: 198.51.100.31
node-external-ip: 192.0.2.21
Avec cette configuration, le nœud publie maintenant l’adresse attendue dans kubectl get nodes -o wide.
Dans un design comme celui-ci, l’idée est justement de pouvoir publier l’adresse applicative comme InternalIP tout en gardant, si nécessaire, une adresse de gestion séparée comme ExternalIP.
Ce qu’elle ne change pas à elle seule : l’interface sur laquelle Cilium attache ses programmes eBPF. C’est un autre sujet.
VXLAN et tunnelendpoint
En mode encapsulé, Cilium construit un maillage de tunnels entre nœuds. La documentation Cilium sur le routage décrit ce mode overlay et rappelle que VXLAN utilise l’UDP 8472.
L’idée importante pour la suite est la suivante :
- le pod d’arrivée n’est pas joint directement par L3 ;
- le paquet est encapsulé vers un endpoint de tunnel porté par le nœud distant ;
- si cet endpoint correspond à la mauvaise interface du nœud, tout le chemin du dataplane est décalé.
Dans la pratique, c’est visible dans le BPF IPCache que l’on peut inspecter via cilium bpf ipcache list ou cilium-dbg bpf ipcache list, décrit dans la documentation de commande Cilium. C’est là que l’on retrouve les tunnelendpoint utilisés par Cilium.
CiliumNodeConfig, devices et direct-routing-device
Cilium permet d’appliquer une configuration par groupe de nœuds via CiliumNodeConfig. La documentation officielle sur la configuration par nœud explique le principe : on surcharge une partie de la configuration de l’agent pour un ensemble de nœuds sélectionnés par label.
apiVersion: cilium.io/v2
kind: CiliumNodeConfig
metadata:
name: worker-app-interface
namespace: kube-system
spec:
nodeSelector:
matchLabels:
node-role.kubernetes.io/worker: "true"
defaults:
devices: "appli"
direct-routing-device: "appli"
Le comportement des options vient de l’agent Cilium lui-même. La référence cilium-agent précise que :
--devicessélectionne les interfaces réseau sur lesquelles l’agent va s’appuyer pour des fonctions comme BPF NodePort, le masquage BPF ou le host firewall ;--direct-routing-devicedésigne explicitement l’interface utilisée pour les décisions de routage direct si l’auto-détection n’est pas suffisante.
Avec cette configuration, le dataplane Cilium s’attache à la bonne interface et prend ses décisions de routage au bon endroit.
Dans une architecture où masters et workers n’utilisent pas la même interface réseau pour le dataplane, cela implique souvent deux CiliumNodeConfig distincts, pas un seul manifest global.
Ce qu’elle ne change pas : la valeur InternalIP du nœud Kubernetes. Si cette InternalIP pointe encore vers la patte d’administration, le problème peut rester entier.
Pourquoi rp_filter=2 compte dans un cluster multi-NIC
Le guide ip-sysctl du noyau Linux documente rp_filter. En mode strict, le noyau attend qu’un paquet reçu ait aussi son meilleur chemin de retour par la même interface. Dans un cluster multi-interface avec encapsulation, cette hypothèse devient fragile.
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
Avec ce réglage, le noyau passe en mode loose et évite de dropper silencieusement des paquets valides dans une topologie asymétrique.
Ce qu’il ne change pas : l’endpoint VXLAN choisi par Cilium. rp_filter=2 évite une classe de drops, mais ne corrige pas un mauvais ancrage du tunnel.
Le scénario à qualifier
Une fois le cluster créé via Rancher, le point à qualifier était simple :
- des pods répartis sur deux workers distincts ;
- un dataplane Cilium attaché à l’interface applicative des workers ;
- une policy interdisant l’accès au réseau d’administration pour les workloads utilisateurs ;
- des flux pod-to-pod inter-workers qui continuent pourtant à fonctionner parce qu’ils passent bien par la patte applicative.
Le détail qui va compter plus tard est le suivant : en provisionnement via l’UI Rancher, il est facile d’enrôler les workers via leur interface de gestion, mais beaucoup moins de leur injecter dès l’enrôlement un node-ip différent et propre à chaque nœud. Le cluster démarre donc dans un état plausible pour la gestion, sans garantie que le dataplane final suive déjà la bonne patte réseau.
Comment le problème s’est manifesté
Le symptôme n’est pas apparu pendant l’installation. Il est apparu au moment de qualifier les flux.
Nous avons déployé des pods de test sur deux workers distincts, puis rejoué des échanges simples entre eux. À ce moment-là :
- les nœuds remontaient en
Ready; kubectl get nodes -o widene montrait rien d’absurde ;- les pods système étaient présents ;
cilium statusrestait propre ;- les workloads applicatifs démarraient.
Mais les échanges inter-workers échouaient. Dans notre cas, la première manifestation exploitable a été un échec de connectivité de type ping ou requête inter-pods, avec des drops visibles côté politique réseau.
Du premier échec à la preuve réseau décisive
Les signaux généraux restaient verts. Le basculement s’est fait en comparant l’InternalIP publiée et l’endpoint VXLAN réel.
- 1
Test inter-worker
Deux pods répartis sur des workers différents échouent à communiquer alors que le cluster semble installé correctement.
- 2
Faux signaux verts
Ready, kubectl get nodes -o wide et cilium status rassurent, mais ne disent rien sur la vraie cible VXLAN.
- 3
Drops visibles côté policy
Hubble et les policies orientent l’attention vers le réseau d’administration, là où le flux n’aurait jamais dû passer.
- 4
Comparaison décisive
Le diagnostic se débloque quand on confronte `InternalIP` et `tunnelendpoint` dans le BPF IPCache.
- 5
Correction et requalification
Après le `node-ip` sur la patte applicative, les endpoints VXLAN et les tests inter-workers se réalignent.
Pourquoi les premiers signaux semblaient pourtant corrects
Le piège de ce type d’incident, c’est que plusieurs contrôles utiles ne répondent pas à la bonne question.
kubectl get nodes -o wide indique qu’un cluster fonctionne, mais pas quel réseau Cilium utilise réellement comme endpoint de tunnel.
cilium status est indispensable, mais il peut confirmer un agent sain sans prouver que le VXLAN inter-workers est ancré sur la bonne adresse. On peut très bien lire un état du genre :
KubeProxyReplacement: True [appli 198.51.100.31 (Direct Routing)]
…et avoir quand même un tunnelendpoint qui pointe vers l’adresse d’administration du worker distant.
De la même manière, une annotation cilium.io/ipv4-node cohérente avec la patte applicative peut exister sans que cela ne modifie, dans la combinaison de versions observée, l’endpoint réellement utilisé dans le BPF IPCache.
L’observation qui a débloqué le diagnostic
Le diagnostic a progressé quand nous avons arrêté de regarder les signaux “verts” et commencé à comparer deux choses très précises :
- l’adresse
InternalIPannoncée par les workers ; - l’adresse de
tunnelendpointutilisée par Cilium pour joindre les pods du worker distant.
Avant correction, on observait un écart de ce type :
$ kubectl get nodes -o wide
NAME STATUS ROLES INTERNAL-IP EXTERNAL-IP
worker-01 Ready <none> 192.0.2.21 <none>
worker-02 Ready <none> 192.0.2.22 <none>
$ cilium bpf ipcache list
10.42.5.0/24 tunnelendpoint=192.0.2.22
10.42.5.144/32 tunnelendpoint=192.0.2.22
Le cluster était sain du point de vue Kubernetes. Le dataplane, lui, s’orientait vers la patte d’administration.
À partir de là, le comportement des policies devenait logique. Si une CiliumClusterwideNetworkPolicy interdit aux workloads utilisateurs de sortir vers le réseau d’administration, alors un flux inter-worker encapsulé vers 192.0.2.22 sera bloqué, même si l’intention fonctionnelle était “autoriser app-a vers app-b”.
Le même cluster, deux ancrages réseau radicalement différents
À gauche, le tunnel vise l’interface d’administration et rencontre la policy. À droite, le `node-ip` réaligne l’endpoint VXLAN avec la patte applicative.
La cause racine, formulée proprement
Une fois cette comparaison faite, la chaîne causale devient beaucoup plus claire :
- les workers sont enrôlés via Rancher à partir de leur interface d’administration ;
- RKE2 publie alors une
InternalIPqui correspond à cette interface ; - Cilium reprend cette
InternalIPcomme endpoint VXLAN dans son BPF IPCache ; - le trafic inter-workers part donc vers le réseau d’administration ;
- la policy censée protéger ce réseau se met à bloquer un flux qui n’aurait jamais dû y passer.
Autrement dit, la policy n’était pas fautive. Elle rendait visible une erreur d’ancrage du dataplane.
Pourquoi cilium.io/ipv4-node et devices: appli ne suffisaient pas
Deux pistes semblaient naturelles, et c’est précisément pour cela qu’elles méritent d’être expliquées.
L’annotation cilium.io/ipv4-node
Elle donnait une information correcte sur l’adresse applicative souhaitée. Dans notre cas, cela ne suffisait pas à faire basculer les tunnelendpoint observés dans le BPF IPCache. Le point de vérité restait l’InternalIP Kubernetes.
devices: appli
Cette configuration restait indispensable. Sans elle, le dataplane des workers ne s’attachait pas au bon endroit. Mais elle ne répondait pas à la même question que node-ip.
node-ipagit sur l’adresse annoncée par le nœud ;devicesagit sur l’attachement du dataplane ;direct-routing-devicecomplète cette configuration et évite, dans notre cas, l’erreurunable to determine direct routing device.
On peut donc avoir une situation intermédiaire où :
cilium statusmontre bienapplicôté worker ;- le remplacement de kube-proxy fonctionne ;
- mais le VXLAN continue malgré tout à viser l’adresse portée par
admin.
La correction retenue
La correction n’a pas consisté à forcer encore davantage Cilium. Elle a consisté à modifier l’adresse que les workers annonçaient à Kubernetes.
Concrètement, les workers ont reçu un override node-ip vers leur patte applicative, après le provisionnement du cluster :
# /etc/rancher/rke2/config.yaml.d/99-nodeip-app.yaml
node-ip: 198.51.100.31
Avec ce fichier, worker-01 publie désormais son adresse applicative comme InternalIP.
Ce qu’il ne change pas : la façon dont Cilium choisit ses interfaces d’attachement. Le CiliumNodeConfig reste nécessaire.
En parallèle, la configuration Cilium côté workers devait rester explicite :
apiVersion: cilium.io/v2
kind: CiliumNodeConfig
metadata:
name: worker-app-interface
namespace: kube-system
spec:
nodeSelector:
matchLabels:
node-role.kubernetes.io/worker: "true"
defaults:
devices: "appli"
direct-routing-device: "appli"
Avec cette configuration, l’agent Cilium des workers travaille bien sur la patte applicative.
Ce qu’elle ne change pas : l’InternalIP du nœud. Sans le node-ip précédent, le tunnel peut rester mal orienté.
Pourquoi le control plane n’a pas cassé
Changer l’InternalIP d’un worker vers la patte applicative ressemble, au premier abord, à une très mauvaise idée. Dans notre cas, cela n’a pas cassé le chemin de gestion pour deux raisons.
La première est visible dans la documentation RKE2 :
- la page Cluster Access montre que le kubeconfig généré par RKE2 pointe vers
127.0.0.1; - la référence agent RKE2 documente
lb-server-port, c’est-à-dire le load-balancer client local utilisé par l’agent.
Autrement dit, le nœud ne parle pas directement à un master distant en utilisant sa propre InternalIP comme point d’entrée. RKE2 interpose déjà un chemin local pour l’accès au supervisor et à l’API server.
La seconde raison est d’architecture : le réseau d’administration reste présent et continue de porter les flux de gestion, de Rancher et du control plane. Le fait de publier une autre InternalIP sur le worker corrige donc le dataplane VXLAN sans déplacer toute la gestion sur la patte applicative.
Deux chemins distincts : gestion locale RKE2 d’un côté, VXLAN applicatif de l’autre
Le worker ne change pas de mission réseau. Il continue à gérer le control plane via le proxy local RKE2, pendant que le dataplane inter-workers s’appuie sur `node-ip` et `CiliumNodeConfig`.
Comment nous avons validé la correction
La validation ne s’est pas arrêtée au retour d’un cluster Ready. Nous avons cherché une chaîne de preuves.
1. Vérifier le basculement de InternalIP
Après correction, les workers devaient publier leur patte applicative comme InternalIP :
$ kubectl get nodes -o wide
NAME STATUS ROLES INTERNAL-IP EXTERNAL-IP
worker-01 Ready <none> 198.51.100.31 192.0.2.21
worker-02 Ready <none> 198.51.100.32 192.0.2.22
Ce contrôle prouve que Kubernetes voit maintenant la bonne adresse comme identité interne du worker.
Il ne prouve pas encore que Cilium l’utilise déjà comme endpoint VXLAN. Il faut aller un cran plus bas.
2. Vérifier que Cilium reste sain
cilium status devait rester cohérent :
- agents en bonne santé ;
- workers attachés à
appli; - absence de crash lié à
direct-routing-device.
Ce contrôle prouve que la configuration Cilium tient.
Il ne prouve toujours pas que le tunnel est ancré sur la bonne IP.
3. Vérifier le BPF IPCache
Le signal décisif se trouvait là :
$ cilium bpf ipcache list
10.42.5.0/24 tunnelendpoint=198.51.100.32
10.42.5.144/32 tunnelendpoint=198.51.100.32
Cette fois, le tunnelendpoint du worker distant pointait vers la patte applicative. C’était le changement attendu.
4. Rejouer les tests inter-workers
La dernière étape consistait à vérifier le comportement observable :
- les échanges inter-workers cessaient d’échouer ;
- les drops auparavant corrélés à la policy d’administration disparaissaient ;
- la séparation administration / applicatif / stockage redevenait lisible dans les flux réels.
Quand ces trois niveaux s’alignent, le sujet est réellement corrigé :
- identité du nœud ;
- endpoint VXLAN ;
- comportement applicatif.
Quatre vérifications avant de conclure
InternalIP, état Cilium, endpoint VXLAN et test inter-worker doivent tous pointer dans la même direction.
Ce qu’il faut retenir pour un autre projet
Trois points ressortent de ce cas.
1. Ready ne veut pas dire “dataplane qualifié”
Un cluster peut être exploitable côté API et encore faux côté chemin réseau inter-workers. Tant que l’on n’a pas regardé les endpoints réellement utilisés par Cilium, une partie critique du système reste implicite.
2. node-ip et CiliumNodeConfig résolvent deux problèmes différents
node-ip agit sur l’identité réseau annoncée par le nœud. CiliumNodeConfig agit sur le dataplane que l’agent Cilium attache et pilote. Les deux sont complémentaires. Les confondre fait perdre du temps.
3. En multi-NIC, la vraie source de vérité est souvent plus bas que kubectl
Sur ce type de sujet, les commandes qui rassurent trop vite sont :
kubectl get nodes -o widepris isolément ;cilium statuspris isolément ;- la simple présence d’annotations ou de manifests supposés corrects.
Les commandes qui font avancer le diagnostic sont :
- la lecture de l’
InternalIPréelle des workers ; - l’inspection du BPF IPCache ;
- les tests inter-workers sur des pods effectivement répartis ;
- l’observation Hubble quand une policy semble “casser” quelque chose.
Conclusion
Le point le plus utile de ce retour d’expérience n’est pas seulement la correction elle-même. C’est la façon de raisonner sur un cluster multi-interface.
Si l’objectif est de faire sortir le dataplane inter-workers sur une patte applicative dédiée, il faut vérifier trois couches distinctes :
- l’adresse que le nœud annonce à Kubernetes ;
- l’interface sur laquelle Cilium attache réellement son dataplane ;
- l’endpoint VXLAN que Cilium utilise pour joindre le nœud distant.
Tant que ces trois couches ne sont pas alignées, un cluster peut paraître sain tout en échouant exactement là où l’architecture devait être qualifiée.
Aimen E.
DevOps and Principal Cloud ArchitectAimen accompagne les clients sur leurs projets de transformation digitale, d'optimisation de la performance applicative et d'adoption du cloud.