Flannel, flannel, flannel…
Flannel est un CNI simple et populaire 😢.
C’est le CNI par défaut de k3s, celui que la moitié des tutos kubeadm utilisent, et on le retrouve aussi dans pas mal d’offres managées.
OK, il est simple, il route les paquets entre les pods, il supporte VXLAN et WireGuard, il se configure en 2 minutes. Que demander de plus ?
Ben justement. Il y a un truc que flannel ne fait pas : les NetworkPolicies. (Et c’est très grave).
Flannel is focused on networking. For network policy, other projects such as Calico can be used.
Et le piège, c’est que si vous n’avez pas lu cette petite ligne dans le README.md, rien ne vous le dit explicitement.
Vous pouvez parfaitement créer des objets NetworkPolicy dans votre cluster, kubectl apply ne bronchera pas, kubectl get netpol vous les listera gentiment. Sauf que… elles ne sont pas enforced. Le trafic passe quand même. Votre deny-all ne deny rien du tout.
On vérifie pour être bien sûr
Avant de résoudre quoi que ce soit, vérifions que le problème existe. En partant du prérequis qu’on a un cluster Kubernetes qui a flannel comme CNI et que tout est fonctionnel, on va déployer deux pods dans deux namespaces différents : un client (curl) et un serveur (nginx).
On vérifie que le client peut joindre le serveur :
kubectl exec -n netpol-test-a client -- \
curl -s --max-time 5 http://server.netpol-test-b.svc.cluster.local
On obtient la page d’accueil nginx. Jusque-là, tout est normal. Maintenant, on applique une NetworkPolicy deny-all sur le namespace du serveur :
# 01-deny-all-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
namespace: netpol-test-b
spec:
podSelector: {}
policyTypes:
- Ingress
kubectl apply -f 01-deny-all-ingress.yaml
Et on re-teste :
kubectl exec -n netpol-test-a client -- \
curl -s --max-time 5 http://server.netpol-test-b.svc.cluster.local
Résultat : la page nginx s’affiche toujours. La NetworkPolicy est bien créée (kubectl get netpol -n netpol-test-b la montre), mais elle n’est pas enforced. Le trafic passe comme si de rien n’était.
C’est le comportement attendu avec flannel. Flannel ne fait que du routage L3 (overlay VXLAN ou WireGuard). Il n’implémente pas de contrôleur NetworkPolicy. Les objets existent dans etcd, mais personne ne les traduit en règles de filtrage.
On nettoie la policy avant de passer à la suite :
kubectl delete -f 01-deny-all-ingress.yaml
Les alternatives pour ajouter le support
Pendant longtemps j’ai pensé que c’était une fatalité et que flannel était juste un CNI nul. Je pense toujours que c’est un mauvais choix pour n’importe quelle production.
MAIS récemment, j’ai découvert qu’il était possible de chaîner les CNI au sein d’un même cluster, et ainsi d’avoir un CNI qui gère la majorité des tâches (ici Flannel) et un autre qui se charge d’autres tâches, comme par exemple enforcer des Netpols.
C’est d’ailleurs le principe de Canal, que je connaissais de nom mais que je n’avais jamais exploré. En fait, c’est ni plus ni moins qu’un manifeste qui déploie Flannel comme CNI principal avec Calico pour l’enforcing des NetOK fine, it’s truepols !
Canal was the name of Tigera and CoreOS’s project to integrate Calico and flannel.
Note : le projet GitHub a été archivé en octobre 2025 mais en théorie ça devrait encore fonctionner, si on trouve la doc correcte (pas trouvé, les liens sont KO, mais j’ai pas vraiment cherché non plus).
Vous avez donc compris le principe, on va ajouter un composant qui va watch les objets NetworkPolicy et les traduire en règles de filtrage effectives (iptables, eBPF, nftables…), sans toucher au flannel existant. C’est ce qu’on appelle le “CNI chaining” ou le mode “policy-only”.
Il existe plusieurs solutions :
Calico (Canal), Historiquement, la combinaison flannel + Calico s’appelle “Canal”, dont je viens de parler. Mais le manifeste Canal officiel embarque son propre flannel dans le même DaemonSet que calico-node. Si votre flannel est déjà installé et géré (par vous, par un opérateur, par un provider…), vous ne voulez probablement pas le remplacer. Et l’opérateur Tigera (la méthode Helm “officielle”) ne supporte pas non plus le déploiement en mode policy-only sur un flannel existant. Bref, c’est faisable mais ça nécessite un peu d’effort. Flemme.
kube-router, kube-router peut fonctionner en mode firewall-only (--run-firewall=true) et n’a besoin que d’iptables/ipset. C’est d’ailleurs ce que k3s utilise par défaut pour les NetworkPolicies. C’est la solution la plus légère (a priori ~50 Mo de RAM par node). Vérifiez que votre kernel dispose du module ip_set, sinon ça ne fonctionnera pas.
Cilium en mode CNI chaining, C’est la solution que j’ai retenue et qu’on va détailler. Cilium s’attache aux interfaces veth créées par flannel et ajoute ses programmes eBPF pour le policy enforcement. Pas de dépendance à iptables ou ipset, et en bonus on récupère Hubble pour l’observabilité réseau.
Installer Cilium en mode CNI chaining
Prérequis
- Un cluster Kubernetes fonctionnel avec flannel
helmv3+- Un kernel >= 4.19 (idéalement >= 5.10 pour toutes les features eBPF)
Récupérer la configuration CNI de flannel
Cilium en mode chaining doit connaître la configuration CNI existante. On va la récupérer depuis un node :
kubectl debug node/$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') \
-it --image=busybox -- cat /host/etc/cni/net.d/10-flannel.conflist
Sur mon cluster, ça donne :
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
Notez le champ name (ici cbr0). On en aura besoin.
Créer le ConfigMap de chaining
On va créer un ConfigMap qui reprend la config flannel et y ajoute le plugin cilium-cni en chaining :
apiVersion: v1
kind: ConfigMap
metadata:
name: cni-configuration
namespace: kube-system
data:
cni-config: |-
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "cilium-cni",
"chaining-mode": "generic-veth"
}
]
}
Attention : le champ name doit correspondre à celui de votre conflist flannel. Si le vôtre s’appelle cni0 ou autre chose, adaptez.
Avant d’appliquer, vérifiez aussi que votre CNI utilise bien des interfaces veth (c’est le cas par défaut avec flannel, mais mieux vaut s’en assurer). Depuis un node :
ip -d link | grep veth
Vous devriez voir des interfaces de type veth correspondant à vos pods, par exemple :
103: lxcb3901b7f9c02@if102: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
veth addrgenmode eui64 numtxqueues 1 numrxqueues 1
Si c’est bien le cas, le mode generic-veth de Cilium fonctionnera. On applique :
kubectl apply -f cilium-cni-configmap.yaml
Installer Cilium via Helm
helm repo add cilium https://helm.cilium.io/
helm repo update
Voici les values pour le mode chaining :
# cilium-values.yaml
cni:
chainingMode: generic-veth
customConf: true
configMap: cni-configuration
install: true
routingMode: native
enableIPv4Masquerade: false
enableIPv6Masquerade: false
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
Les points importants :
cni.chainingMode: generic-veth, c’est le mode chaining, Cilium s’attache aux interfaces veth existantescni.customConf: true+cni.configMap, on fournit notre propre config CNIroutingMode: native, flannel gère le routage, pas CiliumenableIPv4Masquerade: false, flannel gère le masqueradinghubble.enabled: true, l’observabilité réseau, c’est le gros bonus de Cilium
helm install cilium cilium/cilium --version 1.19.1 \
--namespace kube-system \
-f cilium-values.yaml
On attend que tout soit prêt :
kubectl rollout status daemonset/cilium -n kube-system --timeout=120s
Vérification
kubectl exec -n kube-system ds/cilium -- cilium status
Ce qui nous intéresse :
Kubernetes: Ok 1.35 (v1.35.0) [linux/amd64]
CNI Chaining: generic-veth
Cilium: Ok 1.19.1
Hubble: Ok
La ligne CNI Chaining: generic-veth confirme que Cilium fonctionne en mode chaining et ne remplace pas flannel.
Important : les pods qui existaient avant l’installation de Cilium ne sont pas automatiquement gérés par Cilium. Il faut les redémarrer pour que Cilium attache ses programmes eBPF. Pensez à faire un kubectl rollout restart de vos workloads de test (ou à les recréer).
Tester les NetworkPolicies
C’est le moment de vérité. On re-applique notre deny-all :
kubectl apply -f 01-deny-all-ingress.yaml
kubectl exec -n netpol-test-a client -- \
curl -s --max-time 5 http://server.netpol-test-b.svc.cluster.local
Résultat : timeout ! Cette fois, la NetworkPolicy est bien enforced. Le trafic est bloqué.
J’ai enchaîné avec les autres scénarios classiques de NetworkPolicy, et tout fonctionne :
- Allow ingress sélectif par namespace, en ajoutant une policy qui autorise le trafic depuis
netpol-test-auniquement, le curl passe depuis ce namespace mais reste bloqué depuisdefault. L’isolation par namespace fonctionne. - Deny-all egress, en bloquant tout le trafic sortant du client, même la résolution DNS est bloquée (timeout immédiat).
- Allow egress sélectif, en autorisant uniquement le DNS (port 53) et le serveur (port 80 dans le namespace
netpol-test-b), le curl vers le serveur passe maiscurl http://example.comreste bloqué.
Bref, ingress, egress, sélectif par namespace, tout marche comme attendu.
Bonus : Hubble, l’observabilité réseau
C’est pour moi le vrai atout de Cilium par rapport aux alternatives. Hubble permet de voir en temps réel les flux réseau et les verdicts de policy :
kubectl exec -n kube-system ds/cilium -- \
hubble observe --namespace netpol-test-b --last 5
Mar 15 13:20:42.287: netpol-test-a/client:40066 (ID:9745) ->
netpol-test-b/server:80 (ID:22271)
policy-verdict:none ALLOWED (TCP Flags: SYN)
On voit le pod source, le pod destination, le port, l’identité Cilium, et le verdict de policy. Quand vous debuggez une NetworkPolicy qui ne se comporte pas comme prévu, c’est vraiment pratique.
Combien ça coûte en ressources ?
Sur mon cluster (3 nodes), voici ce que Cilium consomme juste après installation :
| Composant | Par node | RAM |
|---|---|---|
| cilium agent | oui (DaemonSet) | ~160 Mo |
| cilium-envoy | oui (DaemonSet) | ~22 Mo |
| cilium-operator | non (2 replicas) | ~42 Mo |
| hubble-relay | non (1 replica) | ~16 Mo |
| hubble-ui | non (1 replica) | ~21 Mo |
Soit environ 180 Mo par node pour l’agent + envoy. J’ai pas de point de comparaison par rapport à Calico ou kube-router, mais ça me semble acceptable, et le fait de pouvoir avoir une vision complète sur la totalité des flux avec Hubble justifie largement le surcoût (à mon avis).
Conclusion
Si jamais vous n’avez pas le choix et que vous devez composer avec flannel, et que vous voulez boucher le trou béant de sécurité que représente l’absence d’enforcement des Netpols, sachez donc qu’il est possible de chaîner un autre CNI pour le faire.
À défaut de l’avoir en CNI pour tout (tout bon cluster Kubernetes a Cilium comme CNI), Cilium en mode CNI chaining (generic-veth) est une solution plutôt sympa pour combler ce manque. Il ne touche pas au flannel existant, il s’y greffe. Et en bonus, vous récupérez Hubble pour l’observabilité réseau, ce qui est franchement appréciable.
Have fun :)
