IPv6 adoption continues to accelerate as IPv4 address space is exhausted, and most production servers now need to serve traffic over both protocols simultaneously. RHEL 9 supports dual-stack networking natively through NetworkManager, allowing IPv4 and IPv6 addresses to coexist on the same interface without additional kernel modules. Configuring dual-stack correctly involves not only assigning addresses but also updating DNS, firewall rules, and service listeners. This tutorial covers the complete dual-stack setup on RHEL 9 using nmcli, Nginx, and firewalld.

Prerequisites

  • RHEL 9 server with NetworkManager managing the network interface
  • An IPv6 address or prefix assigned by your provider or router (static or SLAAC)
  • Nginx installed and running
  • firewalld active and managing the interface zone
  • DNS zone access to add AAAA records

Step 1 — Assign IPv4 and IPv6 Addresses with nmcli

Use nmcli to configure both protocols on the primary interface. Replace ens3, the IPv4 address, and the IPv6 address with values appropriate for your environment. Setting ipv6.method manual assigns a static address; use auto for SLAAC.

nmcli con mod ens3 ipv4.method manual 
  ipv4.addresses 203.0.113.10/24 
  ipv4.gateway 203.0.113.1 
  ipv4.dns "8.8.8.8 8.8.4.4"

nmcli con mod ens3 ipv6.method manual 
  ipv6.addresses 2001:db8:1::10/64 
  ipv6.gateway 2001:db8:1::1 
  ipv6.dns "2001:4860:4860::8888 2001:4860:4860::8844"

nmcli con up ens3

Verify both addresses appear on the interface:

ip addr show ens3

Step 2 — Configure DNS A and AAAA Records

Add both record types in your DNS zone so clients can reach your server over either protocol. In a BIND zone file the entries look like:

example.com.    IN  A     203.0.113.10
example.com.    IN  AAAA  2001:db8:1::10
www             IN  A     203.0.113.10
www             IN  AAAA  2001:db8:1::10

After updating the zone, increment the serial number and reload the name server. Confirm DNS resolution resolves both record types:

dig example.com A
dig example.com AAAA

Step 3 — Configure Nginx to Listen on Both Protocols

By default on Linux, a socket listening on 0.0.0.0:80 may or may not accept IPv6 connections depending on the value of net.ipv6.bindv6only. The safest and most explicit approach is to add separate listen directives for each protocol in your Nginx server block:

server {
    listen 80;
    listen [::]:80;

    listen 443 ssl;
    listen [::]:443 ssl;

    server_name example.com www.example.com;

    # ... rest of configuration
}

Reload Nginx after editing:

nginx -t && systemctl reload nginx

Step 4 — Configure firewalld for IPv6 Traffic

firewalld on RHEL 9 manages both iptables and ip6tables rules. Standard service rules (such as --add-service=http) apply to both protocols automatically. For more granular IPv6-specific rules, use rich rules with the family="ipv6" parameter:

# Allow HTTP and HTTPS for both protocols
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https

# Allow a specific IPv6 range to SSH
firewall-cmd --permanent --add-rich-rule='
  rule family="ipv6"
  source address="2001:db8::/32"
  service name="ssh"
  accept'

firewall-cmd --reload

Verify rules are applied:

firewall-cmd --list-all

Step 5 — Test Connectivity on Both Protocols

Test reachability from a remote host that has both IPv4 and IPv6 connectivity. Use ping for ICMPv4 and ping6 (or ping -6) for ICMPv6, and curl with -4 or -6 to force a specific protocol:

ping -c 3 203.0.113.10
ping6 -c 3 2001:db8:1::10

curl -4 http://example.com
curl -6 http://example.com

Check the dual-stack routing table:

ip route show
ip -6 route show

Step 6 — Manage IPv6 Privacy Extensions

Privacy extensions (use_tempaddr) cause Linux to generate temporary, frequently rotating source addresses for outgoing connections, which can complicate logging and whitelisting. For servers, it is generally preferable to disable them so outgoing traffic always originates from the stable assigned address:

# Disable IPv6 privacy extensions persistently
cat >> /etc/sysctl.d/99-ipv6.conf << 'EOF'
net.ipv6.conf.all.use_tempaddr = 0
net.ipv6.conf.default.use_tempaddr = 0
EOF

sysctl -p /etc/sysctl.d/99-ipv6.conf

Verify the setting has taken effect without a reboot:

sysctl net.ipv6.conf.all.use_tempaddr

Conclusion

Your RHEL 9 server is now dual-stack capable, with IPv4 and IPv6 addresses assigned via NetworkManager, DNS records configured for both protocols, Nginx listening on both stacks, and firewalld rules applied for IPv6 traffic. Privacy extensions have been disabled to ensure consistent outgoing source addresses suitable for a server environment.

Next steps: How to Install Certbot and Automate Let’s Encrypt Renewals on RHEL 9, How to Configure NFS over IPv6 on RHEL 9, and How to Set Up BGP Routing with FRRouting on RHEL 9.