Serve IPv6 clients
You can host public services as dual-stack (accessible over both IPv4 and IPv6) even though Syself Autopilot clusters operate as single-stack IPv4 internally. The Hetzner Load Balancer translates incoming IPv6 requests into IPv4 traffic before forwarding them to your nodes, allowing you to serve IPv6 clients worldwide without reconfiguring your applications, CNI, or pods.
How IPv6 translation works
The Hetzner Load Balancer functions as a dual-stack gateway with built-in IPv6-to-IPv4 translation:
- Public ingress is dual-stack: External clients connect to either the IPv4 or IPv6 address allocated to the load balancer.
- Internal forwarding is IPv4: The load balancer translates external IPv6 requests and proxies them to backend nodes over private IPv4.
- Applications stay unchanged: All nodes, pods, and Services operate purely on standard IPv4 without needing an in-cluster IPv6 stack.
flowchart LR C6["IPv6 client"] -->|IPv6| LB["Hetzner load balancer<br/>IPv4 + IPv6"] C4["IPv4 client"] -->|IPv4| LB LB -->|IPv4 translation| N["Node (IPv4 only)"] --> P["Pod"]
The Hetzner Cloud Controller Manager (CCM) automatically allocates both a public IPv4 and a public IPv6 address for every type: LoadBalancer Service, exposing them in status.loadBalancer.ingress. Once you publish the IPv6 address as an AAAA record in your DNS provider, clients should be able to connect over IPv6.
Publish an AAAA record for the load balancer
Create a single-stack Service
Create a normal type: LoadBalancer Service and keep it single-stack IPv4. Do not make it dual-stack. The load balancer handles both families on the outside.
apiVersion: v1
kind: Service
metadata:
name: my-app
annotations:
load-balancer.hetzner.cloud/location: fsn1
spec:
type: LoadBalancer
ipFamilyPolicy: SingleStack
ipFamilies: [IPv4]
selector:
app: my-app
ports:
- port: 80
targetPort: 80
Read back both assigned addresses
The load balancer has one IPv4 and one IPv6 address:
$ kubectl get svc my-app -o jsonpath='{range .status.loadBalancer.ingress[*]}{.ip}{"\n"}{end}'
203.0.113.10
2a01:4f8:1c1e:abcd::1
Publish A and AAAA records
Point your hostname at both addresses. An A record holds the IPv4, an AAAA record holds the IPv6. A dual-stack client prefers the AAAA and connects over IPv6, an IPv4-only client uses the A.
my-app.example.com. A 203.0.113.10
my-app.example.com. AAAA 2a01:4f8:1c1e:abcd::1
Warning
AAAA records must only point to the IPv6 address assigned to a Hetzner Load Balancer. Do not point AAAA records at node IPs, as cluster nodes operate strictly over IPv4 and will reject direct IPv6 traffic.
The load balancer replaces the source address, so an IPv6 client's real address reaches the pod only with PROXY protocol on. See Enable PROXY protocol .
Reach the API server over IPv6
By default, the Kubernetes API server endpoint in your kubeconfig uses the control-plane load balancer's IPv4 address, meaning all kubectl traffic defaults to IPv4. To allow clients to reach the API server over IPv6, you must assign a custom DNS hostname to the control plane using the clusterEndpointHost topology variable. Publish an A record by default, and add an AAAA only if IPv6-only clients need access (see the tradeoff below).
Give the endpoint a DNS name
Set clusterEndpointHost in your Cluster manifest to a fully qualified domain name (FQDN) you manage, such as api.my-cluster.example.com. Using a domain name allows DNS to resolve both IPv4 and IPv6 addresses for the same control-plane endpoint.
Publish the endpoint records
Point the name at the control-plane load balancer's IPv4 (A) and IPv6 (AAAA). An AAAA makes every IPv6 caller show up as 127.0.7.1 in the audit log instead of its own address, so publish it only if IPv6-only clients need access. Publish real public records: the controllers that manage your cluster resolve this same name, so if it does not resolve from Syself's side, the cluster never finishes provisioning and the control plane remains unavailable.
Configure kubeconfig with the domain name
Set the server: field in your kubeconfig to the new hostname (e.g. https://api.my-cluster.example.com:6443). This allows kubectl to resolve AAAA DNS records and establish connections over IPv6.
An AAAA costs client attribution in the audit log. KubeGate still reads the real client address from the load balancer's PROXY header, so a source-CIDR policy matches on it, but that address cannot be carried onto the node's IPv4 connection to the API server, so the audit record shows 127.0.7.1 instead of the caller. This is a supported tradeoff: publish an AAAA when IPv6-only clients need access, and leave it off when auditability is required.
Caution
Add your IPv6 address to the API allowlist before publishing the AAAA. Matching is address-family strict, so an allowlist holding only IPv4 drops your kubectl as soon as it prefers IPv6. See KubeGate: restrict API server access .
Node-level networking
Nodes in Syself Autopilot clusters operate strictly as single-stack IPv4. No in-cluster services listen on IPv6, and any IPv6 subnet allocated by Hetzner is left unconfigured on node network interfaces.
Keep the following operational considerations in mind:
- IPv6-only egress: In-cluster workloads cannot connect to external endpoints that serves only over IPv6.
- Service traffic policies: Because the load balancer translates IPv6 to IPv4 before reaching the node, all Kubernetes Service features (including
externalTrafficPolicy: Local) operate normally over IPv4.
Preserve the client source IP
Stop your pods from seeing the load balancer address instead of the real client, using PROXY protocol or externalTrafficPolicy Local.
Handle hairpinning to a Service's own LB IP
Understand why a pod calling its own Service's external load balancer IP can time out, and why the platform CCM makes it work.