Enabling Server IPv6: From Addressing to Firewall

If your site doesn't respond over IPv6, or you have an AAAA record but the service won't come up, this guide walks through the complete server IPv6 configuration path step by step.

7 min Updated 1 Nov 2026

Your server has IPv6, you've set the AAAA record, but curl -6 https://example.com on the same machine either times out or immediately returns Connection refused. Meanwhile, the IPv4 version of the site works at full speed. This is exactly the point where most server administrators get stuck: they assume that because the IPv6 address is sitting on the interface, everything is done. It isn't. Three separate layers must align, and if one is left behind, you'll see the same misleading symptoms.

First, determine whether server IPv6 is actually enabled or just assigned

Before making any changes, read the current state. The command ip -6 addr show scope global shows only public IPv6 addresses and filters out link-local addresses that start with fe80::. If the output is empty, the problem is at the network layer, not the web server.

ip -6 addr show scope global
ip -6 route show default
sysctl net.ipv6.conf.all.disable_ipv6

The last line should return 0. If it was 1, IPv6 is disabled at the kernel level and no other configuration will work. This state is seen on some older images and certain default security templates. To enable it temporarily:

sysctl -w net.ipv6.conf.all.disable_ipv6=0
sysctl -w net.ipv6.conf.default.disable_ipv6=0

To make it permanent, put these two lines in /etc/sysctl.d/99-ipv6.conf and run sysctl --system. A point many people miss: if you set disable_ipv6 to zero on all but an explicit value of 1 remains on a specific interface, that interface stays disabled. The interface value takes priority over the global value.

Set the address manually or get it from SLAAC

On cloud servers, the address is usually announced from the network side and you just need to accept it. But on a dedicated server where an IPv6 block has been handed to you, addressing is manual. On systemd-networkd-based distributions, write the file /etc/systemd/network/10-static.network like this:

[Network]
Address=2001:db8:1::10/64
Gateway=2001:db8:1::1
DNS=2001:4860:4860::8888

On Ubuntu with Netplan, the equivalent goes in /etc/netplan/01-netcfg.yaml under the addresses and routes keys. If your SSH connection drops after netplan apply, don't panic; come back via the console or rescue mode and fix the configuration. This scenario is so common that it's worth knowing in advance how server reinstallation and working with rescue mode; saving data when you've lost access works.

Set the AAAA record correctly, not just set it

The AAAA record works exactly like A, except its value is a 128-bit address. The syntax in a BIND zone file looks like this:

www     IN  AAAA    2001:db8:1::10
@       IN  AAAA    2001:db8:1::10

Three common mistakes that all produce the same symptoms. First, writing the address in uncompressed form or with uppercase letters, which some DNS panels reject. Second, using :: twice in one address, which is syntactically invalid. Third, putting a link-local address — that is, the fe80:: one — in the AAAA record. This address is not routable from outside and clients can't reach it.

To check, use dig and explicitly request the AAAA type:

dig +short AAAA example.com @1.1.1.1
dig +trace AAAA example.com

If dig responds but the browser doesn't, the problem isn't DNS. The problem is the return path or the firewall.

Here's where they go wrong: treating the IPv6 firewall separately

This is the most common mistake I see in tickets. The server administrator opens port 443 in iptables, the IPv4 site comes up, and they assume the job is done. But iptables only sees IPv4 packets. IPv6 traffic passes through ip6tables or the inet table in nftables. The result? The site opens over IPv4, times out over IPv6, and because browsers have Happy Eyeballs, the user usually doesn't notice anything odd until a visitor with an IPv6-only network arrives and the page doesn't load at all.

The diagnostic sign is this: ss -6 -tlnp shows the service listening on :::443, but curl -6 from outside times out. That means the service is healthy and the firewall is blocking it. To see the current rules:

nft list ruleset
ip6tables -L -n -v --line-numbers

If you have nftables on the system, iptables and ip6tables are just a compatibility layer and may not show the real rules. See the complete command reference in Linux firewall commands: a quick reference for iptables, nftables, and ufw so you don't get confused between the three.

Force the service to listen on IPv6

Many services listen only on IPv4 by default. In Nginx, put the following line inside the server block:

listen [::]:443 ssl http2;
listen [::]:80;

The brackets are mandatory; without them Nginx errors out because : in an IPv6 address is itself a separator. In Apache, just add Listen [::]:443. For MySQL and PostgreSQL, change the bind-address parameter to ::, but only do this if you genuinely need remote IPv6 access. Exposing the database on :: without a firewall repeats the same IPv4 disaster. The list of ports that should never be open is in a reference of common service ports and ports that should not be open.

After the change, restart the service and verify that it's actually listening on IPv6:

ss -6 -tlnp | grep -E ':(80|443)'
curl -6 -I https://example.com
curl -6 -v telnet://[2001:db8:1::10]:443

If the service won't come up and there's nothing in the log, you probably edited the unit manually and systemd didn't read it. The correct structure and how to restart safely are explained in the systemd unit file: sections, restart, and dependencies.

Choosing between dual-stack IPv6 and IPv6-only

There are two real paths ahead of you, and the choice depends on the use case, not on preference.

CriterionDual-Stack configurationIPv6-only
Compatibility with current usersCompleteRequires NAT64 or a proxy
Firewall complexityTwo rule setsOne rule set
Risk of configuration errorHighLow
Suitable forPublic site, storeInternal service, API backend

For any site with anonymous users, I choose dual-stack. The reason is simple: a significant portion of corporate networks and domestic ISPs still don't have an IPv6 path, and if you serve only IPv6, you lose them. But for an internal API between microservices, IPv6-only is the right choice. One rule set, no NAT, no confusion over translated addresses. The cost is that every monitoring tool and every old script must understand IPv6; some Python scripts have socket.AF_INET hardcoded and fail silently.

A limitation few people know about in advance

If your server is behind NAT64 or behind a 6to4 tunnel, the source address your service sees isn't the same as the client's real address. Any rate-limit or IP-based blocking logic goes haywire in this case. Before deployment, watch traffic for a few minutes with tcpdump -i eth0 -n ip6 and see what the source addresses look like. If they all come from a specific prefix, you're probably behind a translator.

For the final test from outside, use online IPv6 test services or run curl -6 from a machine on a different network. Testing from the server itself never proves the return path. If you're working on cloud infrastructure, some of these settings may have been done at the network level and just need to be confirmed in the operating system; on a cloud server the address is usually already routed and you have less work to do. On dedicated hardware, you go through all these steps yourself.

Frequently asked questions

Why does the site open over IPv4 but not over IPv6?

It's almost always the firewall. iptables rules don't apply to IPv6 traffic, and you must configure ip6tables or the inet table in nftables separately. Test it: if ss -6 -tlnp shows the service listening on :::443 but curl -6 from outside times out, the service is healthy and the firewall is blocking it.

Does removing the AAAA record solve the problem?

Temporarily, yes, but it's not a solution. By removing AAAA, IPv6-only users are completely lost, and some browsers behave oddly for several hours due to DNS caching. If you're in a hurry and the site must come up right now, removing the AAAA record is an acceptable temporary measure; but set the TTL to 300 seconds so you can quickly revert it later.

Where should I get the IPv6 address from?

On cloud servers, it's usually announced from the network side and you just need to accept it. On a dedicated server, an IPv6 block is delivered to you along with the service and addressing is manual. In both cases, before anything else, check with ip -6 addr show scope global that the address is actually sitting on the interface.

How do I tell whether the IPv6 firewall is to blame or the service itself?

On the server itself, connect to loopback with curl -6 http://[::1]:443. If it responds, the service is healthy and the problem is in the network path or firewall. If it doesn't respond, the service isn't listening on IPv6 and you need to add the listen [::] line to the configuration.

The next step is clear: right now, run ip -6 addr show scope global and nft list ruleset and see which layer is empty. In ninety percent of cases, the answer is found in those two outputs.

Was this page helpful?