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 / timeout

Initial 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:

LayerWhat to configureStatus
Talos/Node OSStatic IPv6 on interfaceDone via patches
KubernetesPod/Service CIDRsMissing!
Ciliumipv6.enabled: trueDone

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 only

The 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 CIDR

The 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:

  1. The base controlplane.yaml (with the new IPv6 CIDRs)
  2. 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.yaml

Node 2:

talosctl apply-config --talosconfig talos/talosconfig \
  --endpoints <node2-ip> --nodes <node2-ip> \
  --file talos/controlplane.yaml \
  --config-patch @talos/patches/ipv6-node2.yaml

Node 3:

talosctl apply-config --talosconfig talos/talosconfig \
  --endpoints <node3-ip> --nodes <node3-ip> \
  --file talos/controlplane.yaml \
  --config-patch @talos/patches/ipv6-node3.yaml

The --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-system

Wait 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-system

Verification

Check Envoy is binding to both IPv4 and IPv6

kubectl exec -n kube-system ds/cilium -- ss -tlnp | grep 443

Expected 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 gateway

Should 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>]:443

Both 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::1

Key points:

  • dhcp: true - Keep IPv4 DHCP
  • addresses - Static IPv6 from your provider
  • gateway: 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: true

Why 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:443 AND [::]: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:

  1. Talos nodes: Static IPv6 addresses via per-node patches
  2. Kubernetes: IPv6 CIDRs in cluster.network.podSubnets and serviceSubnets
  3. 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

FilePurpose
talos/controlplane.yamlBase config with dual-stack pod/service CIDRs
talos/patches/ipv6-node1.yamlStatic IPv6 for node 1
talos/patches/ipv6-node2.yamlStatic IPv6 for node 2
talos/patches/ipv6-node3.yamlStatic IPv6 for node 3
k8s/cilium-values.yamlCilium config with ipv6.enabled: true

Written by Afanasy Barbarov — Tech Lead with 15+ years shipping production systems in Rust, Go, and TypeScript. Facing a similar challenge? Reach out on LinkedIn. Support my work.

More articles

Previous post

Migrating Apps from Docker Swarm to Kubernetes.

Read more