IPv6 gives every server globally routable addresses without NAT, and a growing share of users and networks reach services over it first. Most servers are assigned an IPv6 prefix by their provider, but the address still has to be configured on the interface, allowed through the firewall and published in DNS before it is useful. In this tutorial you will configure a static IPv6 address and default route alongside IPv4 (dual stack) with Netplan on Ubuntu 24.04 and with nmcli on Rocky Linux 9, then verify connectivity and make your services reachable over IPv6.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS or Rocky Linux 9, for example a CubePath VPS, with working IPv4 connectivity.
- A non-root user with
sudoprivileges. - The IPv6 address, prefix length and gateway assigned to your server by your provider.
- Access to an out-of-band console (VNC or serial console) in case a mistake drops your SSH session.
This guide uses the IPv6 documentation prefix. Replace these example values with your own:
| Setting | Example value |
|---|---|
| Interface | eth0 |
| IPv6 address and prefix | 2001:db8:1234::10/64 |
| IPv6 gateway | 2001:db8:1234::1 |
| IPv6 DNS resolvers | 2606:4700:4700::1111, 2620:fe::fe |
IPv6 addressing in brief
A few rules explain most of what you will see in the command output:
- An IPv6 address has 128 bits written as eight groups of hexadecimal digits. A run of zero groups can be shortened once with
::, so2001:db8:1234:0:0:0:0:10is written2001:db8:1234::10. - Servers usually receive a
/64(or larger) prefix. You can use any address inside it; the first 64 bits identify the network. - Every IPv6 interface also has a link-local address starting with
fe80::. It is created automatically, only works on the local network segment, and is often used as the gateway address (for examplefe80::1). - Addresses are either configured statically or learned automatically from router advertisements (SLAAC). Servers normally use static addresses so that the address never changes.
Step 1 - Checking the current IPv6 state
Make sure IPv6 is not disabled in the kernel. A value of 0 means IPv6 is enabled:
sysctl net.ipv6.conf.all.disable_ipv6
net.ipv6.conf.all.disable_ipv6 = 0
If the value is 1, look for the setting that disables it in /etc/sysctl.conf and /etc/sysctl.d/, remove it, and run sudo sysctl --system. Also check that the kernel command line does not contain ipv6.disable=1 with cat /proc/cmdline.
List the IPv6 addresses already on the interface:
ip -6 addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 fe80::be24:11ff:fe12:3456/64 scope link
valid_lft forever preferred_lft forever
An interface with only a scope link address has no public IPv6 configured yet. If you already see a scope global address, your image configured it for you and you can skip to Step 4 to verify it.
Step 2 - Configuring IPv6 with Netplan on Ubuntu 24.04
Netplan keeps the network configuration in YAML files under /etc/netplan/. Find the file that defines your interface:
sudo grep -l eth0 /etc/netplan/*.yaml
On cloud images this is usually /etc/netplan/50-cloud-init.yaml, which cloud-init may regenerate at boot. To keep your manual changes, first stop cloud-init from managing the network:
echo 'network: {config: disabled}' | sudo tee /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
Then open the file that defines the interface:
sudo nano /etc/netplan/50-cloud-init.yaml
Add the IPv6 address to the addresses list and a second default route through the IPv6 gateway. A complete dual-stack configuration with a static IPv4 address looks like this:
network:
version: 2
ethernets:
eth0:
dhcp4: false
dhcp6: false
accept-ra: false
addresses:
- 203.0.113.10/24
- "2001:db8:1234::10/64"
routes:
- to: default
via: 203.0.113.1
- to: default
via: "2001:db8:1234::1"
nameservers:
addresses:
- 1.1.1.1
- 2606:4700:4700::1111
- 2620:fe::fe
The example assumes a static IPv4 address. If your file uses dhcp4: true, keep that line and leave out the IPv4 address and route; only the IPv6 entries are new. accept-ra: false stops the server from adding extra addresses or routes from router advertisements, so the configuration you wrote is the only one in effect. If your provider tells you to use SLAAC instead of a static address, set accept-ra: true and remove the static IPv6 address and route.
Two gateway variants are common:
- If the gateway is a link-local address such as
fe80::1, use it asviaexactly as shown. Link-local addresses are always reachable on the interface. - If the gateway is a global address outside your prefix, add
on-link: trueunder that route so the kernel knows it is directly reachable.
Quote IPv6 addresses in YAML when they contain ::, so the parser never misreads them. Restrict the file permissions, check the syntax, and apply with automatic rollback:
sudo chmod 600 /etc/netplan/*.yaml
sudo netplan generate
sudo netplan try
Press ENTER within 120 seconds if your SSH session still responds. Otherwise the previous configuration is restored automatically. Continue with Step 4.
Step 3 - Configuring IPv6 with nmcli on Rocky Linux 9
On Rocky Linux 9, NetworkManager manages the interface through a connection profile. Find the profile bound to your interface:
nmcli connection show
NAME UUID TYPE DEVICE
System eth0 5fb06bd0-0bb0-7ffb-45f1-d6edd65f3e03 ethernet eth0
Set a static IPv6 address, gateway and resolvers on that profile. Use the profile name from your output:
sudo nmcli connection modify "System eth0" \
ipv6.method manual \
ipv6.addresses 2001:db8:1234::10/64 \
ipv6.gateway 2001:db8:1234::1 \
ipv6.dns "2606:4700:4700::1111 2620:fe::fe"
Reactivate the profile to apply the change:
sudo nmcli connection up "System eth0"
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/2)
Confirm what NetworkManager stored:
nmcli connection show "System eth0" | grep -E '^ipv6\.(method|addresses|gateway):'
ipv6.method: manual
ipv6.addresses: 2001:db8:1234::10/64
ipv6.gateway: 2001:db8:1234::1
Step 4 - Verifying IPv6 connectivity
Check that the global address is present:
ip -6 addr show dev eth0 scope global
inet6 2001:db8:1234::10/64 scope global
valid_lft forever preferred_lft forever
Check the IPv6 default route:
ip -6 route show default
default via 2001:db8:1234::1 dev eth0 proto static metric 1024 pref medium
Ping the gateway, then an external IPv6 host:
ping -6 -c 3 2001:db8:1234::1
ping -6 -c 3 2606:4700:4700::1111
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
Confirm that DNS resolution and HTTPS work over IPv6. The response is the address your traffic comes from, which must be your server's IPv6 address:
curl -6 https://icanhazip.com
2001:db8:1234::10
Step 5 - Allowing IPv6 through the firewall
A firewall that only has IPv4 rules can leave services unreachable over IPv6, or worse, unprotected.
On Ubuntu with UFW, make sure IPv6 support is enabled in /etc/default/ufw:
grep ^IPV6 /etc/default/ufw
IPV6=yes
With IPV6=yes (the default), every rule you add applies to both protocols. sudo ufw status lists each rule twice, once with (v6):
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
If you changed the value to yes, reload the firewall with sudo ufw reload. UFW already allows the ICMPv6 messages that IPv6 needs to work (neighbor discovery, router advertisements, packet-too-big); do not block ICMPv6 entirely.
On Rocky Linux, firewalld applies the zone rules to IPv4 and IPv6 at the same time, so no extra step is needed. Check the services allowed in the active zone with sudo firewall-cmd --list-all.
Step 6 - Publishing services over IPv6
Services must listen on IPv6 addresses and have an AAAA record in DNS before clients can use them.
Check which services listen on IPv6. Entries with [::] or your IPv6 address accept IPv6 connections:
sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=812,fd=3))
LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=812,fd=4))
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=940,fd=6))
In this example SSH listens on both protocols but Nginx only on IPv4. For Nginx, add an IPv6 listen directive to each server block, next to the IPv4 one:
server {
listen 80;
listen [::]:80;
server_name your_domain;
}
Test the configuration and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Finally, create an AAAA record for your_domain pointing to your IPv6 address at your DNS provider, and check it once it propagates:
dig +short AAAA your_domain
2001:db8:1234::10
From a machine with IPv6 connectivity, curl -6 http://your_domain should now return your site.
Troubleshooting
ping -6 returns Network is unreachable: there is no IPv6 default route. Check ip -6 route show default and the gateway in your configuration.
The gateway does not answer (Destination unreachable: Address unreachable): the gateway address is wrong, or it is outside your prefix and missing on-link: true in Netplan. Compare it with the values your provider assigned.
The address shows tentative or dadfailed: duplicate address detection found another host using the same address. Pick a different address in your prefix, or confirm with your provider that the address is assigned to you.
An extra, unexpected IPv6 address appears on the interface: router advertisements added a SLAAC address in addition to your static one. Set accept-ra: false in Netplan and apply again. With nmcli, ipv6.method manual already ignores SLAAC addresses.
IPv6 works on the server but clients cannot reach a service: check that the service listens on [::] (Step 6), that the firewall rule has a (v6) counterpart, and that the AAAA record points to the right address.
Conclusion
Your server now has a static IPv6 address and default route alongside IPv4, the firewall covers both protocols, and your services listen on IPv6 with an AAAA record that clients can resolve.
As next steps, you can:
- Set a reverse DNS (PTR) record for the IPv6 address, especially if the server sends email.
- Add more addresses from your
/64to theaddresseslist to run several services on dedicated IPs. - Monitor your service over IPv6 as well as IPv4, since a broken AAAA record silently affects IPv6 users.
