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 sudo privileges.
  • 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:

SettingExample value
Interfaceeth0
IPv6 address and prefix2001:db8:1234::10/64
IPv6 gateway2001:db8:1234::1
IPv6 DNS resolvers2606: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 ::, so 2001:db8:1234:0:0:0:0:10 is written 2001: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 example fe80::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 as via exactly as shown. Link-local addresses are always reachable on the interface.
  • If the gateway is a global address outside your prefix, add on-link: true under 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 /64 to the addresses list 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.