Dual-Stack IPv6 with Cilium Gateway API
by Afanasy Barbarov
Dual-Stack IPv6 with Cilium Gateway API
How to get native IPv6 working with Cilium Gateway API on Talos Linux. No proxies, no workarounds - just proper configuration at all three layers.
The Problem
After setting up Cilium Gateway API with hostNetwork mode, IPv4 worked fine but IPv6 didn't:
# IPv4 - works
curl -kv https://<node-ipv4>:443
# TLS handshake, response
# IPv6 - fails
curl -kv https://[<node-ipv6>]:443
# Connection refused / timeoutInitial diagnosis pointed to Cilium bug #37978 - supposedly Envoy only binds to 0.0.0.0 in dual-stack mode. I even tried workarounds like creating two Gateway resources or deploying a socat forwarder.
None of that worked. The real issue was elsewhere.
The Real Problem
The Cilium operator logs revealed the truth:
level=error msg="Unable to create Service" error="Service is invalid:
spec.ipFamilies[1]: Invalid value: \"IPv6\": not configured on this cluster"Kubernetes itself wasn't configured for dual-stack. Three layers need IPv6 enabled:
| Layer | What to configure | Status |
|---|---|---|
| Talos/Node OS | Static IPv6 on interface | Done via patches |
| Kubernetes | Pod/Service CIDRs | Missing! |
| Cilium | ipv6.enabled: true | Done |
The cluster was bootstrapped with only IPv4 CIDRs:
# talos/controlplane.yaml (BEFORE)
cluster:
network:
podSubnets:
- 10.244.0.0/16 # IPv4 only
serviceSubnets:
- 10.96.0.0/12 # IPv4 onlyThe Fix
Step 1: Add IPv6 CIDRs to Talos cluster config
Edit talos/controlplane.yaml:
cluster:
network:
podSubnets:
- 10.244.0.0/16
- fd00:10:244::/48 # ADD: IPv6 pod CIDR
serviceSubnets:
- 10.96.0.0/12
- fd00:10:96::/112 # ADD: IPv6 service CIDRThe fd00::/8 prefix is for unique local addresses (ULA) - the IPv6 equivalent of RFC1918 private addresses. Safe for internal cluster use.
Step 2: Apply config WITH IPv6 patches to each node
This is the tricky part. Each node needs:
- The base
controlplane.yaml(with the new IPv6 CIDRs) - Its node-specific IPv6 patch (static address + route)
Node 1:
talosctl apply-config --talosconfig talos/talosconfig \
--endpoints <node1-ip> --nodes <node1-ip> \
--file talos/controlplane.yaml \
--config-patch @talos/patches/ipv6-node1.yamlNode 2:
talosctl apply-config --talosconfig talos/talosconfig \
--endpoints <node2-ip> --nodes <node2-ip> \
--file talos/controlplane.yaml \
--config-patch @talos/patches/ipv6-node2.yamlNode 3:
talosctl apply-config --talosconfig talos/talosconfig \
--endpoints <node3-ip> --nodes <node3-ip> \
--file talos/controlplane.yaml \
--config-patch @talos/patches/ipv6-node3.yamlThe --config-patch @file.yaml syntax merges the patch into the base config before applying.
Step 3: Restart Cilium
kubectl rollout restart daemonset cilium -n kube-system
kubectl rollout restart deployment cilium-operator -n kube-system
kubectl rollout restart daemonset cilium-envoy -n kube-systemWait for rollouts to complete:
kubectl rollout status daemonset cilium -n kube-system
kubectl rollout status deployment cilium-operator -n kube-system
kubectl rollout status daemonset cilium-envoy -n kube-systemVerification
Check Envoy is binding to both IPv4 and IPv6
kubectl exec -n kube-system ds/cilium -- ss -tlnp | grep 443Expected output:
LISTEN 0 4096 0.0.0.0:443 0.0.0.0:*
LISTEN 0 4096 [::]:443 [::]:*Both 0.0.0.0:443 (IPv4) and [::]:443 (IPv6) should be listening.
Check Gateway has no errors
kubectl logs -n kube-system deployment/cilium-operator --tail=50 | grep -i gatewayShould show "Successfully reconciled Gateway" without "IPv6 not configured" errors.
Test both protocols
# IPv4
curl -kv https://<node-ipv4>:443
# IPv6
curl -kv https://[<node-ipv6>]:443Both should return TLS handshake with your certificate.
The Node-Specific IPv6 Patches
Each node needs its own static IPv6 address (some providers don't support DHCPv6/SLAAC).
Example for node 1 (talos/patches/ipv6-node1.yaml):
machine:
network:
interfaces:
- interface: ens3
dhcp: true
addresses:
- <node1-ipv6>/64
routes:
- network: ::/0
gateway: fe80::1Key points:
dhcp: true- Keep IPv4 DHCPaddresses- Static IPv6 from your providergateway: fe80::1- Link-local gateway (provider-specific)
Cilium Values for Dual-Stack
Make sure k8s/cilium-values.yaml has:
ipv6:
enabled: true
ipv4:
enabled: true
enableIPv6Masquerade: true
gatewayAPI:
enabled: true
hostNetwork:
enabled: trueWhy the "Cilium Bug" Wasn't
I initially blamed Cilium bug #37978 which says "Envoy only binds to 0.0.0.0 in dual-stack hostNetwork mode."
But with proper Kubernetes dual-stack configuration:
- Envoy binds to both
0.0.0.0:443AND[::]:443 - No socat/nginx proxy needed
- No separate IPv4/IPv6 Gateway resources needed
- It just works
The bug may exist in some edge cases, but for my setup, the problem was simply missing IPv6 CIDRs at the Kubernetes level.
Summary
To get dual-stack IPv6 working with Cilium Gateway API:
- Talos nodes: Static IPv6 addresses via per-node patches
- Kubernetes: IPv6 CIDRs in
cluster.network.podSubnetsandserviceSubnets - Cilium:
ipv6.enabled: true
All three layers must be configured. Missing any one causes failures:
- Missing node IPv6 → ping6 fails, curl times out
- Missing K8s CIDRs → "IPv6 not configured on this cluster" error
- Missing Cilium IPv6 → Cilium ignores IPv6 entirely
Files
| File | Purpose |
|---|---|
talos/controlplane.yaml | Base config with dual-stack pod/service CIDRs |
talos/patches/ipv6-node1.yaml | Static IPv6 for node 1 |
talos/patches/ipv6-node2.yaml | Static IPv6 for node 2 |
talos/patches/ipv6-node3.yaml | Static IPv6 for node 3 |
k8s/cilium-values.yaml | Cilium config with ipv6.enabled: true |