Un cluster full IPv6 avec Talos
Dans le cadre de mon auto-hébergement, je me mets de plus en plus à m’intéresser à l’IPv6 (indépendamment de mon ASN, pas encore très utilisé). Sans forcément dire que je suis un fan de l’IPv6, je trouve que c’est une bonne chose de se roder dessus, et de s’y habituer, surtout face à l’épuisement des adresses IPv4.
Jusqu’à présent, je limitais mon usage de la version 6 du protocole IP à des usages basiques sur des machines Linux individuelles, mais je n’avais jamais tenté de déployer un cluster Kubernetes full IPv6.
Habitués du blog, je vais vous faire l’affront de vous parler une énième fois de Talos OS, un système d’exploitation minimaliste et sécurisé conçu spécifiquement pour exécuter des clusters Kubernetes. Si vous ne connaissez pas encore Talos, je vous invite à lire un de mes articles à ce sujet. Talos se pilote essentiellement via une CLI (talosctl) et des fichiers de configuration en YAML.
Par chance, je dispose d’un /60 IPv6 grâce à mon FAI, ce qui est LARGEMENT suffisant pour déployer un cluster Kubernetes (et grâce à ça, je peux utiliser des réelles plages de mon réseau, sans devoir recourir à des IPs link-local). Dans cet article, on ne va pas creuser les détails d’IPv6, mais plutôt se concentrer sur la configuration de Talos pour qu’il utilise uniquement des adresses IPv6.
On commence ?
Démarrer les nodes Talos en IPv6
Planification des plages IPv6
Déjà, je suis dans un réseau avec un DHCPv4 qui tourne, l’idée est quand même de ne pas s’en servir pour avoir un cluster single-stack IPv6 (voire même de désactiver l’IPv4 sur les nodes Talos, mais on verra ça plus tard).

Par ailleurs, il s’agit d’une adresse IPv6 commençant par “fe80::”, c’est une adresse link-local (c’est à dire qu’elle n’est valable que sur le lien local, et pas routable). Je préfère utiliser des adresses IP globales, je dispose d’un /64 que je vais séparer en 2 parties : Une pour les nodes, une pour le CIDR des pods, et un dernier pour le CIDR des services.
Préfixe des nodes : 2a01:e0a:5b7:4a40::/64
CIDR des pods : 2a01:e0a:5b7:4a41::/64
CIDR des services : 2a01:e0a:5b7:4a42::/64
Déjà, soyons d’accord que mettre des /64 est vraiment overkill puisque ça supporte 18 quintillions d’adresses, mais bon, qui sait si je vais pas ouvrir 250 datacenters un jour ?
Un dernier point un peu dommage : Talos n’activant pas le DHCPv6 par défaut, il faut une première étape où la machine récupère une adresse IPv4 pour qu’on puisse lui envoyer la configuration Talos via talosctl.
Configuration initiale avec talhelper
Générons ensuite la configuration Talos avec talhelper (un outil permettant de faciliter la création des fichiers de configuration Talos) :
---
clusterName: ipv6-only-talos-cluster
endpoint: https://[2a01:e0a:5b7:4a40::11]:6443 # Devrait être un LoadBalancer en front des control planes
allowSchedulingOnControlPlanes: true
cniConfig:
name: none
nodes:
- hostname: talos-ipv6-only-01
controlPlane: true
ipAddress: 192.168.1.138
installDisk: /dev/sda
nameservers:
- 2001:4860:4860::8888
patches:
- |-
machine:
network:
interfaces:
- interface: ens18
dhcp: false
addresses:
- "2a01:e0a:5b7:4a40::11/128"
routes:
- network: ::/0
gateway: 2a01:e0a:5b7:4a40::1
- hostname: talos-ipv6-only-02
controlPlane: true
ipAddress: 192.168.1.64
installDisk: /dev/sda
nameservers:
- 2001:4860:4860::8888
patches:
- |-
machine:
network:
interfaces:
- interface: ens18
dhcp: false
addresses:
- "2a01:e0a:5b7:4a40::12/128"
routes:
- network: ::/0
gateway: 2a01:e0a:5b7:4a40::1
- hostname: talos-ipv6-only-03
controlPlane: true
ipAddress: 192.168.1.18
installDisk: /dev/sda
nameservers:
- 2001:4860:4860::8888
patches:
- |-
machine:
network:
interfaces:
- interface: ens18
dhcp: false
addresses:
- "2a01:e0a:5b7:4a40::13/128"
routes:
- network: ::/0
gateway: 2a01:e0a:5b7:4a40::1
patches:
- |-
cluster:
network:
podSubnets:
- 2a01:e0a:5b7:4a41::/64
serviceSubnets:
- 2a01:e0a:5b7:4a42::/64
proxy:
disabled: true
machine:
kubelet:
nodeIP:
validSubnets:
- 2a01:e0a:5b7:4a40::/60
Voici les points notables de cette configuration :
- Chaque node Talos a une adresse IPv6 statique dans le préfixe
2a01:e0a:5b7:4a40::/64. - On paramètre les adresses IPv6 des nodes dans les patches machine.network.interfaces.
- Les adresses IPv4 citées ne sont pas utilisées par Talos mais uniquement par
talosctl.
Dès que la configuration sera appliquée, les machines perdront leur adresse IPv4 et ne fonctionneront qu’en IPv6.
talhelper genconfig
talhelper gencommand apply --extra-flags --insecure
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=192.168.1.138 --file=./clusterconfig/ipv6-only-talos-cluster-talos-ipv6-only-01.yaml --insecure;
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=192.168.1.64 --file=./clusterconfig/ipv6-only-talos-cluster-talos-ipv6-only-02.yaml --insecure;
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=192.168.1.18 --file=./clusterconfig/ipv6-only-talos-cluster-talos-ipv6-only-03.yaml --insecure
Plus qu’à attendre la fin de l’installation ⏳… sauf si ça marche pas 😅, c’est l’heure de dig ça !
Résolution des problèmes d’IPv6 avec Talos
talosctl dmesg -n 2a01:e0a:5b7:4a40::11 --talosconfig ./clusterconfig/talosconfig
error getting dmesg: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp [2a01:e0a:5b7:4a40::11]:50000: connect: connection refused"
Ça ne marche pas. En fait l’API de Talos est inaccessible durant la phase d’installation, il faut donc aller voir les logs dans l’hyperviseur (ici Proxmox) :

La raison est simple : Le node Talos n’arrive pas à résoudre factory.talos.dev en IPv6 (l’IPv4 marche, mais pas l’IPv6) :
dig AAAA factory.talos.dev +short
# Pas de réponse
Bon beh c’était rapide ! Premier problème que l’on rencontre en IPv6 avec Talos : L’accès à l’image factory.talos.dev.
Lorsqu’on génère une configuration Talos avec talhelper, celui-ci va injecter une image factory dans la configuration, qui est utilisée lors de l’installation initiale de Talos. Par défaut, cette image est hébergée sur factory.talos.dev… qui n’est pas accessible en IPv6.
cat ./clusterconfig/ipv6-only-talos-cluster-talos-ipv6-only-03.yaml | yq .machine.install.image
factory.talos.dev/metal-installer/376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba:v1.11.6
L’alternative serait de pointer vers une image officielle (comme ghcr.io/siderolabs/installer:v1.11.5) venant de ghcr.io, mais là encore, ghcr.io n’est pas accessible en IPv6 (voir plus bas).
dig AAAA ghcr.io +short
# Pas de réponse
Remarque
En revanche, utiliser l’image ghcr.io/siderolabs/installer:v1.11.5 contraint à utiliser des images sans extension, c’est pas forcément idéal.
L’image factory, pas accessible en IPv6, ghcr non plus… il ne reste plus qu’à héberger les images Docker localement.
Sur mon poste de travail, j’ai Docker d’installé, je vais donc lancer un registry Docker local (sur le port 6000 pour ne pas interférer avec un éventuel registry local sur le port 5000) :
docker run -d -p 6000:5000 --restart always --name registry registry:2
Je peux ensuite puller l’image factory depuis Docker Hub (en forçant la plateforme linux/amd64, car mon poste est en ARM) :
docker pull factory.talos.dev/metal-installer/ce4c980550dd2ab1b17bbf2b08801c7eb59418eafe8f279833297925d67c7515:v1.11.6 --platform linux/amd64
docker tag factory.talos.dev/metal-installer/ce4c980550dd2ab1b17bbf2b08801c7eb59418eafe8f279833297925d67c7515:v1.11.6 127.0.0.1:6000/talos:v1.11.6
docker push 127.0.0.1:6000/talos:v1.11.6
Même si j’utilise mon IP locale 127.0.0.1, je peux joindre ma registry Docker en IPv6 via l’adresse 2a01:e0a:5b7:4a40::b9ac:5c86 (adresse IPv6 de mon poste dans mon réseau).
curl -6 -I http://\[2a01:e0a:5b7:4a40::b9ac:5c86\]:6000
HTTP/1.1 200 OK
Cache-Control: no-cache
Date: Sun, 21 Dec 2025 14:38:52 GMT
Ce que je peux faire maintenant, c’est modifier la configuration Talos pour utiliser mon registry local pour l’image factory :
- hostname: talos-ipv6-only-01
controlPlane: true
ipAddress: 192.168.1.138
talosImageURL: "[2a01:e0a:5b7:4a40::b9ac:5c86]:6000/talos" # On ajoute cette ligne
installDisk: /dev/sda
nameservers:
- 2001:4860:4860::8888
patches:
- |-
machine:
network:
interfaces:
- interface: ens18
dhcp: false
addresses:
- "2a01:e0a:5b7:4a40::11/128"
routes:
- network: ::/0
gateway: 2a01:e0a:5b7:4a40::1
Appliquons et …

Talos force l’usage du HTTPS, ce qui est plutôt bien mais en l’occurrence ça ne m’aide pas trop là. Ce qui me donne 2 options :
- Soit je configure un record DNS + certificat TLS valide pour mon registry local (via Let’s Encrypt par exemple)
- Soit j’ai la flemme et je passe par une registry compatible IPv6.
Le savez-vous ? Docker Hub est accessible en IPv6 😜.
docker tag factory.talos.dev/metal-installer/ce4c980550dd2ab1b17bbf2b08801c7eb59418eafe8f279833297925d67c7515:v1.11.6 qjoly/talos:v1.11.6
docker push qjoly/talos:v1.11.6
Désolé pour les puristes, on fera plus propre la prochaine fois !
Ce qui donne le nouveau diff :
- hostname: talos-ipv6-only-01
controlPlane: true
ipAddress: 192.168.1.138
talosImageURL: "qjoly/talos" # On utilise Docker Hub cette fois
installDisk: /dev/sda
nameservers:
- 2001:4860:4860::8888
patches:
- |-
machine:
network:
interfaces:
- interface: ens18
dhcp: false
addresses:
- "2a01:e0a:5b7:4a40::11/128"
routes:
- network: ::/0
gateway: 2a01:e0a:5b7:4a40::1
Maintenant, on réapplique la configuration Talos et …
2a01:e0a:5b7:4a40::11: user: warning: [2025-12-22T15:13:28.278570127Z]: level=info msg=fetch failed error=failed to do request: Head "https://
ghcr.io/v2/siderolabs/kubelet/manifests/v1.34.1": dial tcp 140.82.121.33:443: connect: network is unreachable host=ghcr.io image=ghcr.io/sider
olabs/kubelet:v1.34.1
Et c’est pas forcément mieux : Talos essaye de récupérer l’image kubelet depuis ghcr.io, qui n’est pas accessible en IPv6.
J’ai donc deux options :
- Soit je repousse toutes les images Talos sur Docker Hub.
- Soit je monte une registry Docker locale avec toutes les images Talos.
Le saviez-vous ? Il n’est pas nécessaire d’avoir une registry avec du TLS pour ces images (le saviez-vous aussi : mon docker hub free-tier est pas illimité). Beh cette fois on va utiliser notre registry locale.
Les images utilisées par Talos peuvent être listées avec la commande talosctl image default.
➜ talosctl image default
ghcr.io/siderolabs/flannel:v0.27.4
registry.k8s.io/coredns/coredns:v1.12.4
gcr.io/etcd-development/etcd:v3.6.5
registry.k8s.io/kube-apiserver:v1.34.1
registry.k8s.io/kube-controller-manager:v1.34.1
registry.k8s.io/kube-scheduler:v1.34.1
registry.k8s.io/kube-proxy:v1.34.1
ghcr.io/siderolabs/kubelet:v1.34.1
ghcr.io/siderolabs/installer:v1.11.5
registry.k8s.io/pause:3.10
Parmi ces images, seules celles hébergées sur ghcr.io posent problème (registry.k8s.io est aussi accessible en IPv6).
On va utiliser cette commande pour puller toutes les images Talos localement, puis les taguer pour notre registry locale, et enfin les pousser.
for image in `talosctl image default`; do
if [[ $image == ghcr.io/* ]]; then
docker pull $image --platform linux/amd64;
docker tag $image `echo $image | sed -E 's#^[^/]+/#127.0.0.1:6000/#'`;
docker push `echo $image | sed -E 's#^[^/]+/#127.0.0.1:6000/#'`
fi
done
Mais l’avoir sur notre registry locale ne suffit pas, il faut aussi dire à Talos d’utiliser cette registry locale pour ces images. Voici le patch à ajouter dans la configuration Talos :
machine:
registries:
mirrors:
ghcr.io:
endpoints:
- http://[2a01:e0a:5b7:4a40::b9ac:5c86]:6000/
Appliquons une dernière fois la configuration Talos, et cette fois ça devrait le faire !
talosctl kubeconfig
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
talos-ipv6-only-01 Ready control-plane 4m8s v1.34.1 2a01:e0a:5b7:4a40::11 <none> Talos (v1.11.6) 6.12.62-talos containerd://2.1.5
talos-ipv6-only-02 Ready control-plane 4m8s v1.34.1 2a01:e0a:5b7:4a40::12 <none> Talos (v1.11.6) 6.12.62-talos containerd://2.1.5
talos-ipv6-only-03 Ready control-plane 4m6s v1.34.1 2a01:e0a:5b7:4a40::13 <none> Talos (v1.11.6) 6.12.62-talos containerd://2.1.5
Et voilà, un cluster Kubernetes full IPv6 avec Talos OS ! 🎉
KUBECONFIG=kubeconfig kubectl run -i --tty debug -n kube-system --image=nicolaka/netshoot -- curl -6 -I https://perdu.com
If you don't see a command prompt, try pressing enter.
HTTP/2 200
date: Sun, 21 Dec 2025 15:39:35 GMT
content-type: text/html
last-modified: Thu, 02 Jun 2016 06:01:08 GMT
cache-control: max-age=600
expires: Sun, 21 Dec 2025 15:49:35 GMT
vary: Accept-Encoding,User-Agent
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
server: cloudflare
alt-svc: h3=":443"; ma=86400
Conclusion / Ce qu’il reste à faire
Eh bien voilà, on a bien un cluster Kubernetes single-stack IPv6 qui tourne avec Talos OS. Bon, ce n’est pas forcément très utile dans un contexte domestique (encore que), mais c’est un bon exercice pour se familiariser avec l’IPv6 et Talos.
Il reste encore des problèmes à résoudre :
- Se passer de Flannel au profit de Cilium.
- Des petits problèmes côté Talos dans sa gestion des gateways réseaux.
- KubePrism qui semble dans les choux si on désactive l’IPv4 (moins important cela dit).
Troubleshooting Cilium en IPv6
Pour cilium, j’ai essayé de l’installer mais sans succès pour l’instant, les agents ciliums attendent un pool que j’ai déjà défini mais sans succès.
KUBECONFIG=kubeconfig cilium install \
--set ipam.mode=kubernetes \
--set kubeProxyReplacement=true \
--set securityContext.capabilities.ciliumAgent="{CHOWN,KILL,NET_ADMIN,NET_RAW,IPC_LOCK,SYS_ADMIN,SYS_RESOURCE,DAC_OVERRIDE,FOWNER,SETGID,SETUID}" \
--set securityContext.capabilities.cleanCiliumState="{NET_ADMIN,SYS_ADMIN,SYS_RESOURCE}" \
--set cgroup.autoMount.enabled=false \
--set cgroup.hostRoot=/sys/fs/cgroup \
--set k8sServiceHost=2a01:e0a:5b7:4a40::11 \
--set k8sServicePort=6443 \
--set cgroup.hostRoot=/sys/fs/cgroup \
--helm-set ipv6.enabled=true \
--helm-set ipv4.enabled=false \
--helm-set ipv6NativeRoutingCIDR=2a01:e0a::/32 \
--helm-set ipam.operator.clusterPoolIPv6PodCIDRList='{2a01:e0a:5b7:4a41::/64}' \
--helm-set enableIPv6Masquerade=false \
--helm-set routingMode=native
Une fois les pods déployés, seul l’agent Cilium du premier control-plane arrive à se lancer tandis que les autres restent bloqués avec un message d’erreur en log cilium-agent time=2025-12-21T16:53:10.820547938Z level=warn msg="Waiting for k8s node information" module=agent.controlplane.daemon error="required IPv6 PodCIDR not available".
➜ talos-ipv6 kubectl get pods -n kube-system -l app.kubernetes.io/name=cilium-agent -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cilium-8828v 0/1 Running 0 4m59s 2a01:e0a:5b7:4a40::13 talos-ipv6-only-03 <none> <none>
cilium-fvb7c 0/1 Running 0 4m59s 2a01:e0a:5b7:4a40::12 talos-ipv6-only-02 <none> <none>
cilium-zwdg6 1/1 Running 0 4m59s 2a01:e0a:5b7:4a40::11 talos-ipv6-only-01 <none> <none>
Ça attendra l’année prochaine pour creuser ce point !
Avec Talos, étrangement j’ai des erreurs de routing venant du fait que j’injecte la passerelle (Gateway) dans le machineConfig .. sachant qu’il possède déjà une route par défaut venant de l’adresse SLAAC. Voici l’erreur dans les logs :
2a01:e0a:5b7:4a40::11: user: warning: [2025-12-21T16:35:52.196649262Z]: [talos] controller failed {"component": "controller-runtime", "controller": "network.RouteSpecController", "error
": "1 error occurred:\n\t* error adding route: netlink receive: file exists, message {Family:10 DstLength:0 SrcLength:0 Tos:0 Table:0 Protocol:4 Scope:0 Type:1 Flags:0 Attributes:{Dst:<nil> Src:<nil> Gateway:2a01:e0a:5b7:4a40::1 OutIface:8 Priority:1024 Table:254 Mark:0 Pref:<nil> Expires:<nil> Metrics:<nil> Multipath:[]}}\n\n"}
NODE NAMESPACE TYPE ID VERSION DESTINATION GATEWAY LINK METRIC
2a01:e0a:5b7:4a40::11 network RouteStatus ens18/inet6/fe80::3a07:16ff:fe22:a77//1024 1 fe80::3a07:16ff:fe22:a77 ens18 1024
Pour autant, comme vu plus haut, le cluster fonctionne parfaitement et les pods arrivent à sortir sur Internet en IPv6. J’ai adressé ce problème à l’équipe Sidero sur Slack et apparemment la 1.12 de Talos devrait apporter le nécessaire pour gérer ce cas.
Sait-on jamais, peut-être que cet article aidera quelqu’un à se lancer dans l’IPv6 avec Talos OS (et dans ce cas ça serait un plaisir d’avoir vos feedbacks !).
