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.
```mermaid
flowchart LR
C6(["IPv6 client"]):::you -->|IPv6| LB["Hetzner load balancer
IPv4 + IPv6"]:::external
C4(["IPv4 client"]):::you -->|IPv4| LB
LB -->|IPv4 translation| N["Node (IPv4 only)"]:::platform --> P["Pod"]:::app
```
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.
```yaml title="service.yaml"
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:
```console
$ 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`.
```text
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](/docs/hetzner/apalla/network/load-balancing/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](/docs/hetzner/apalla/security/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.