You can accept IPv6 connections from the internet without running dual-stack inside the cluster. The Hetzner load balancer bridges IPv6 to IPv4, so every node and pod remains IPv4-only and you change nothing in your application.
## How the bridge works
The load balancer performs the IPv6-to-IPv4 translation. IPv6 clients reach the load balancer, and the connection from the load balancer to the node is still IPv4. Every node, pod, and Service remains IPv4, and the node never sees IPv6.
```mermaid
flowchart LR
C6["IPv6 client"] -->|IPv6| LB["Hetzner load balancer
IPv4 + IPv6"]
C4["IPv4 client"] -->|IPv4| LB
LB -->|IPv4| N["Node (IPv4 only)"] --> P["Pod"]
```
You do not configure this. The CCM already assigns every `type: LoadBalancer` Service both a public IPv4 and a public IPv6 address, and both appear in `status.loadBalancer.ingress`. You publish the IPv6 address in DNS, and the setup is complete.
## 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]
> Only publish an `AAAA` record for a load-balancer-fronted address. Never point an `AAAA` record at a node's IP. Nodes have no IPv6 address and cannot accept IPv6 connections.
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
Your `kubectl` reaches the API server over IPv6 only when the control-plane endpoint is a DNS name. By default the endpoint is the load balancer's IPv4 literal, so every client connects over IPv4. The `clusterEndpointHost` topology variable holds this value and is empty by default.
Give the endpoint a DNS name
Set `clusterEndpointHost` on the `Cluster` object to a name you control, for example `api.my-cluster.example.com`. A name, rather than an IP, is what lets you publish both families for one endpoint.
Publish A and AAAA for the endpoint name
Point the name at the control-plane load balancer's IPv4 (`A`) and IPv6 (`AAAA`). 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.
Add the IPv6 address to the API certificate
Only the load balancer's IPv4 is currently in the API server certificate. Adding the IPv6 address to the certificate's Subject Alternative Names is not a topology variable, so contact Syself support to request it. Until then, an IPv6 client must use a hostname already in the certificate, or skip TLS verification.
Point the kubeconfig at the name
Set the kubeconfig `server:` field to the DNS name, not an IP, so the client resolves `A`/`AAAA` and selects a family.
> [!CAUTION]
> Check your API allowlist before you publish the `AAAA`. If you restrict the API server with a source-address allowlist, matching is address-family strict: an IPv4 range never matches an IPv6 client. Once the name has an `AAAA` and your `kubectl` prefers IPv6, an allowlist that lists only IPv4 drops you, because the gate fails closed. Add your IPv6 address to the allowlist first. See [Restrict API server access](/docs/hetzner/apalla/security/restrict-api-server-access).
## Nodes have no IPv6 stack
The bridge is safe because nodes have no IPv6 stack. On an IPv4-only cluster no service can listen on IPv6, and the IPv6 range Hetzner routes to the server is never used. Do not change this on IPv4-only nodes.
Two consequences follow from IPv4-only nodes: a pod cannot reach a destination that publishes only IPv6 (almost every host also publishes IPv4, so this is rare), and both `externalTrafficPolicy` values work over IPv6 because the node's peer is always the load balancer over IPv4.