Le retour de la vengeance
Vous avez peut-être lu mon article précédent sur etcd il y a 3 ans. Si vous vous en souvenez bien, le crash, c’était etcd, mais le vrai coupable, c’était Kyverno. J’adore Kyverno. C’est vraiment un logiciel que j’aime beaucoup. Notamment parce que c’est puissant(issime). J’ai d’ailleurs écrit un article d’intro et un second qui va plus loin sur le sujet.
Mais le nombre d’incidents et de side effects chelou que ça provoque. Mamamia… C’est pas le premier incident de l’année que j’ai avec Kyverno (oui, on est en février) mais comme celui-là est rigolo, je vous le partage.
Lors d’une opération de maintenance de routine pour mettre à jour un cluster Kubernetes vers la version 1.34 (depuis la 1.32), on s’est retrouvé face au scénario redouté par tout admin kube : un API Server totalement injoignable au redémarrage des nœuds du Control Plane.
Ce qui ressemblait initialement à une erreur réseau classique s’est avéré être un deadlock subtil entre les nouvelles fonctionnalités réseau natives de Kubernetes et notre cher Kyverno 😘.
Spoiler : c’était pas un problème réseau. C’est jamais un problème réseau. Enfin si, des fois. Mais pas cette fois.
L’upgrade qui commence bien
Bon, un upgrade de Kubernetes, c’est devenu assez banal à ce stade. On fait ça régulièrement, on a nos procédures, on est des pros (si si). On passe de la 1.32 à la 1.34 en un seul commit, sans faire le petit saut par la 1.33 comme recommandé.
TKT FRR.
Dans le contexte technique dont je parle, tout est géré as code. De la création des machines jusqu’au déploiement de Talos, en passant par les MachineConfig (les CustomResources pour modifier… et bien, la machine).
Pour plus d’infos, voir la documentation Talos sur les Machine Configs.
Le premier cluster qu’on teste n’a qu’un seul control plane (me demandez pas pourquoi, ça n’aurait probablement rien changé). Talos relance l’API Server dans la nouvelle version et là… rien.
Les logs de l’API Server “chelous” (terme technique) parlent d’eux-mêmes :
I0224 15:32:50.979280 1 default_servicecidr_controller.go:166] Creating default ServiceCIDR with CIDRs: [10.1.0.0/20]
W0224 15:32:50.984784 1 dispatcher.go:225] rejected by webhook "validate.kyverno.svc-fail":
admission webhook "validate.kyverno.svc-fail" denied the request:
Get "https://10.1.0.1:443/api": dial tcp 10.1.0.1:443: connect: operation not permitted
I0224 15:32:50.985342 1 event.go:389] "Event occurred" kind="ServiceCIDR"
apiVersion="networking.k8s.io/v1" type="Warning"
reason="KubernetesDefaultServiceCIDRError"
message="The default ServiceCIDR can not be created"
😬😬😬
La root cause : un magnifique cercle vicieux
Après investigation, on a découvert que l’incident était le résultat d’une collision entre une évolution du core de Kubernetes et notre configuration Kyverno. Un beau cas d’école de deadlock.
Décomposons le mécanisme :
- Le nouveau Kind
ServiceCIDR
Dans les versions récentes (v1.33+), Kubernetes migre la gestion des plages d’IP de services vers des objets dédiés nommés ServiceCIDR. Au premier démarrage après l’upgrade, l’API Server tente de créer automatiquement l’objet par défaut (par exemple 10.1.0.0/20).
Pour les curieux, la KEP-1880 et la documentation officielle du ServiceCIDR détaillent cette évolution.
C’est nouveau, c’est propre, c’est bien pensé. Sauf que…
- Interception par le Webhook Kyverno
Kyverno, configuré avec une failurePolicy: Fail (parce qu’on est des gens sérieux qui ne laissent pas passer n’importe quoi en prod), est programmé pour intercepter les créations de ressources afin de les valider, et faire échouer le call si Kyverno ne répond pas.
Y compris le ServiceCIDR fraîchement créé par l’API Server lui-même.
- Deadlock
Et c’est là que ça devient magnifique (“la douleur peut être une source de plaisir” - Mozinor) :
- L’API Server suspend la création du
ServiceCIDRen attendant le “OK” de Kyverno - Pour contacter le service Kyverno, l’API Server doit router la requête via l’IP du service Kubernetes (en général
10.1.0.1) - Sauf que la couche réseau (routage vers les services) ne peut pas s’initialiser tant que l’objet
ServiceCIDRn’est pas validé et créé
C’est l’œuf et la poule, version “j’ai les clés de la voiture dans la voiture fermée à clé”.
PTSD. Oui ça m’est arrivé. Dans le désert. Sans réseau.
- Profit.
L’API Server tombe en timeout ou renvoie une erreur connect: operation not permitted en tentant de joindre le webhook, bloquant ainsi sa propre initialisation. CrashLoopBackOff sur l’API Server. :D
Sortir du deadlock
Pour sortir de ce deadlock, il faut bypasser temporairement la couche d’admission. Facile, non ?
Le workaround “habituel” : inutile
Les deadlocks avec Kyverno, on a l’habitude à ce stade. Normalement, comme kube-system est ignoré, on peut simplement se connecter avec un kube-config de secours (breaking glass, on est en OIDC normalement) qui a le cluster role cluster-admin et supprimer les validating webhooks de Kyverno :
kubectl delete validatingwebhookconfiguration kyverno-resource-validating-webhook-cfg
Sauf que là, c’est l’API Server qui ne démarre même pas. Mon kubectl ne risque pas de fonctionner !
Le vrai workaround : désactiver les webhooks au boot
La solution qu’on a choisie a été de modifier la configuration de l’API Server pour désactiver temporairement les webhooks de validation au démarrage. Mon éminent collègue Maxime a édité à chaud le machine config (avec un accès talosctl breaking glass) pour ajouter le flag suivant directement dans les extraArgs de l’API server :
--disable-admission-plugins=ValidatingAdmissionWebhook
Pour ceux qui ne savent pas trop comment ça fonctionne l’admission control dans Kubernetes, sachez juste qu’il y a une liste de plugins “par défaut” mais que tout est débrayable. Je ferai peut-être un jour un deep dive sur l’admission control dans Kubernetes, c’est fascinant ;).
Avec ce flag, l’API Server peut enfin créer ses objets ServiceCIDR sans demander la permission à personne (on bypass complètement tous les mécanismes de validation que Kyverno ou assimilé enforce), le réseau s’initialise, Kyverno démarre, et on peut ensuite retirer le flag et relancer proprement.
L’option “golri” qu’on n’a pas testée
Personnellement, je trouvais très rigolo l’idée d’aller modifier directement la base etcd pour supprimer la clé du webhook qui posait problème (là aussi en passant par talosctl) :
# Exemple via etcdctl
etcdctl del /registry/admissionregistration.k8s.io/validatingwebhookconfigurations/kyverno-resource-validating-webhook-cfg
Mes collègues étaient moins chauds : “Ouais mais tu comprends, si on casse etcd ça va être pénible”. On a joué la sécurité avec le flag. Je suis super déçu, on n’a pas testé 😂.
La solution permanente : les MatchConditions
OK, maintenant que le cluster est reparti, comment on s’assure que ça ne se reproduise pas au prochain upgrade ?
La solution propre consiste à utiliser les matchConditions (introduites en Kubernetes 1.27) au niveau de la ValidatingWebhookConfiguration. Ça permet d’exclure les ressources critiques de bootstrap réseau avant même que la requête ne tente de sortir de l’API Server vers le pod Kyverno.
Voir la doc officielle sur les matchConditions.
On utilisait déjà cette option pour limiter le trafic parfois abusif de Kyverno (si vous administrez du Kyverno, vous savez de quoi je parle) sur un certain nombre d’événements (on écroule l’API server et/ou Kyverno en CPU ou RAM, selon les cas). Il a suffi d’ajouter les exclusions pour les nouveaux types :
# Exclusion des ressources de bootstrap réseau pour éviter le deadlock
matchConditions:
- name: 'exclude-ServiceCIDR'
expression: '!(request.kind.kind == "ServiceCIDR")'
- name: 'exclude-IPAddress'
expression: '!(request.kind.kind == "IPAddress")'
Avec ça, quand l’API Server crée un ServiceCIDR au boot, la requête ne passe même plus par le webhook Kyverno. Pas de dépendance circulaire, pas de deadlock, tout le monde est content.
Conclusion
Comme dirait notre cher (non) président :
qui aurait pu prévoir ?
Bon ok, il suffisait de lire les release notes de Kubernetes 1.33. Cela étant dit, on a un cluster de staging, il sert à ça. On a cassé staging, no big deal

Peut-être qu’on lira quand même, la prochaine fois ?
