Disclosure up front: I run IPYE, a company that leases IP space. It gets one short paragraph at the end; the rest is plain networking.
Adding IPv6 to a web server is mostly four small jobs: put an address on the host, make the web server listen on it, publish an AAAA record, and let the traffic through the firewall. Each step has a common mistake, and the worst one, publishing the AAAA record before the other steps work, breaks the site for IPv6 visitors. This article walks through nginx and Apache and then shows how to test each layer separately. Examples use 2001:db8:100::10 for the server and www.example.com for the site.
Step 1: the address and route
Confirm the host has the address and a default route before touching the web server:
ip -6 addr show dev eth0
ip -6 route show default
ping -6 -c 3 2001:db8:100::1 # the gateway
If the provider gave you a /64 or larger, configure 2001:db8:100::10 with a prefix length that matches the subnet (typically /64) and the gateway they specify.
Step 2: nginx
nginx listens per address family, so you add an IPv6 listen directive next to each IPv4 one:
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name www.example.com;
ssl_certificate /etc/ssl/example/fullchain.pem;
ssl_certificate_key /etc/ssl/example/privkey.pem;
...
}
Details that cause trouble:
- On Linux,
listen [::]:80;by default creates an IPv6-only socket (theipv6only=ondefault). That is what you want, sincelisten 80;already covers IPv4. Writing[::]:80 ipv6only=offand alsolisten 80;conflicts and nginx reports "address already in use". - The
ipv6onlyparameter can be set only once for a given port, on the firstlistenthat defines it. Parameters such asdefault_serverapply perlistendirective, so if you mark the IPv4 listener of a block as the default, mark its IPv6 listener too. - Access rules accept IPv6 syntax directly:
allow 2001:db8:200::/48;. A rule list that only has IPv4 entries silently allows or denies IPv6 clients according to the finaldeny all, so review each. - Behind a proxy or load balancer, set
real_ip_headerandset_real_ip_fromwith both IPv4 and IPv6 ranges of the proxy, or your logs will show the proxy's address.
Check and reload with nginx -t && systemctl reload nginx.
Step 3: Apache
Apache's Listen directive without an address, such as Listen 80, binds to all interfaces. On Linux that normally includes IPv6, with IPv4 connections handled through the same dual-stack socket. To be explicit, or to bind specific addresses, give them separately:
Listen 192.0.2.10:443
Listen [2001:db8:100::10]:443
<VirtualHost 192.0.2.10:443 [2001:db8:100::10]:443>
ServerName www.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/example/fullchain.pem
SSLCertificateKeyFile /etc/ssl/example/privkey.pem
</VirtualHost>
If you use <VirtualHost *:443>, the wildcard already matches IPv6. Address-based access rules use Require ip 2001:db8:200::/48. Validate with apachectl configtest.
Step 4: the firewall
Open the same ports in the IPv6 ruleset. It is common to see iptables rules for 80 and 443 with nothing in ip6tables, or an nftables ip table without an ip6 or inet equivalent. Also keep ICMPv6 (Packet Too Big, in particular) working, otherwise some clients will connect and then hang on large responses.
Step 5: test before publishing the AAAA record
Do not add DNS yet. Test the server by address, with the right hostname, so TLS and virtual hosts still match:
ss -tlnp | grep -E ':(80|443)\b'
curl -6 -I --resolve www.example.com:443:[2001:db8:100::10] https://www.example.com/
ss should show a listener on [::]:443 (or the specific IPv6 address). --resolve makes curl connect to the given address while still sending www.example.com in the SNI field and Host header; the brackets are required for IPv6 literals. Test from a different network than the server, since local tests can succeed through the loopback path and miss firewall problems.
Step 6: publish the AAAA record
Add the record with a modest TTL so you can pull it back quickly:
www.example.com. 300 IN AAAA 2001:db8:100::10
Then query it and test through normal resolution:
dig +short AAAA www.example.com
curl -6 -sS -o /dev/null -w '%{remote_ip} %{http_code}\n' https://www.example.com/
curl -4 -sS -o /dev/null -w '%{remote_ip} %{http_code}\n' https://www.example.com/
Both should return a success code. The two address families should serve identical content: run both checks and compare. Also check that the certificate is the same, since a stray default server on only one family can present a different certificate.
Things that break only on IPv6
- Applications that store or validate IP addresses. Database columns sized for 15 characters, regular expressions that accept only dotted quads, and libraries that mis-parse a colon as a port separator all show up once IPv6 clients arrive. A full textual IPv6 address can be 39 characters long, and forms with an embedded IPv4 address reach 45.
- Rate limiting and bans. One IPv6 user commonly has a /64 or more, so limits keyed on a single /128 are easy to evade. Aggregate by /64 where that fits your policy.
-
Logging and analytics. Log parsers and fail2ban rules need IPv6 patterns. Make sure ban actions apply to
ip6firewall tables. -
Reverse DNS. Mail servers and some services check PTR records; delegate and set
ip6.arpaentries for addresses that send mail. - Hard-coded addresses in configuration or allow lists.
Rolling back
If IPv6 users have trouble, remove the AAAA record. Clients fall back to IPv4 once cached records expire. A low TTL during rollout shortens that window, and you can raise it again once the service has been stable.
About IPYE
IPYE (https://my.ipye.com) offers leased IPv4, IPv6, and ASN space. The first year of a /48 IPv6 block is free, and IPYE handles LOA, ROA, rDNS, and WHOIS. Locations: Los Angeles, New York, Ashburn, Salt Lake City, London, Amsterdam, and Frankfurt. Questions: ipye@turnel.com.
Disclosure: I run IPYE
Top comments (0)