Skip to content

IPv6

Taranac answers IPv4 and IPv6 clients on the same ports, and every service a device or a browser dials sees an IPv6 client by its own address: TACACS+, RADIUS, NAC, the EST and SCEP gateway, the MFA service, the web interface and the captive portal record it, match it against the inventory and count it in their limits by that address. Nothing has to be switched on for this. It needs a host that can carry IPv6 inside Docker, and it changes three things an operator looks after: device inventory entries, the host firewall, and how per-address limits count a client.

The host itself needs no IPv6 address for the stack to run. It needs one, with a route, only to be reached by IPv6 clients.

TermMeaning
Stack networkThe Docker network every container of the node sits on, taranac_default. It carries IPv4 10.255.255.224/27 and the IPv6 ULA fd64:1fbc:c16e::/64.
Client-facing servicesThe containers clients dial from outside: edge, taranac-mfa, tacacs, radius, nac, est-gateway and captive-portal. Only these get an IPv6 address on the stack network.
IPv4-only containersfrontend, api, postgres, taranac-renderer and winbind are kept off IPv6: no IPv6 address and no AAAA record, so a name inside the stack always resolves to IPv4.
Own addressDocker forwards an IPv6 packet for a published port into the container with its source address unchanged, as it does for IPv4. The service sees the device’s address.
Gateway addressWhen Docker’s ip6tables is off, Docker’s userland proxy accepts the IPv6 connection on the host and opens a new one into the container. The service then sees the stack network’s gateway, 10.255.255.225, instead of the client.
/64The unit an IPv6 client is counted in by the per-address limits. An IPv6 host owns a whole /64 and changes address inside it on its own.
RequirementWhy
Docker Engine 28.0 or newerOn older Docker a container kept off IPv6 still gets an IPv6 address and an AAAA record, so connections inside the stack try a dead address first and the api and the portal stall.
Docker Engine, not PodmanPodman answering the docker command (podman-docker) is refused: the stack’s network and compose files are Docker Engine’s.
IPv6 in the kernelThe edge and the AAA daemons listen on IPv6 as well as IPv4 and cannot start without it. A kernel booted with ipv6.disable=1 is refused; IPv6 turned off by sysctl is fine.
Docker able to create a dual-stack networkChecked with a throwaway network with IPv6 beside IPv4. When Docker refuses it, its own error is shown. A frequent cause is the ip6_tables kernel module not being loaded (sudo modprobe ip6_tables), or a firewall manager holding ip6tables.
Docker’s ip6tables on (the default)Not a refusal, a warning: with it off, IPv6 clients reach Taranac from the gateway address.

bootstrap.sh, install.sh and ./taranac update check all of this before they change anything. A refusal says what it found, prints the command that fixes it on this host (the Docker update that matches how Docker was installed, the GRUB change for the kernel, the docker network create test to re-run), and ends with the command to run again. bootstrap.sh keeps a Docker that is already on the host unless it is older than 28.0 or is Podman.

When the host passes, the installer and the updater say so in one line, which the update also writes to .taranac-update.log:

Host: Docker 29.1.2 ✓, kernel IPv6 ✓, dual-stack network test ✓, accept_ra: nothing changed

See Installation → Prerequisites.

What each service does with an IPv6 client

Section titled “What each service does with an IPv6 client”
ServicePortsOver IPv6
TACACS+49, 6049 (TLS)One dual-stack listener per port. A device is matched by its own IPv6 address and needs an inventory entry covering it, with its key; one device record holds one address, so a device that sends over IPv4 and IPv6 needs an entry for each. See IPv6 devices.
RADIUS1812/1813 UDPListens on IPv4 and IPv6. The request belongs to the device whose entry covers its IPv6 source address, which picks the shared secret. See How a request finds its device.
NAC (802.1X)1814/1815 UDPAccepts packets over IPv6 from the device’s own address. Not supported: NAS-IPv6-Address, CoA or Disconnect to a switch over IPv6, and finding a captive portal guest’s MAC address by an IPv6 address. Register switches that need CoA by an IPv4 address.
EST and SCEP gateway8445, 8446Listens on IPv6. Networks devices may request certificates from takes IPv6 networks; the enrollment guard counts an IPv6 source by its /64. See EST and SCEP.
Web interface and API443 / 80 (edge)The audit log, login lockout and rate limits read the browser’s own address.
MFA service8443, or 443 with TARANAC_MFA_DOMAINPhones register and answer pushes over IPv6; the signature ban counts by /64.
Captive portal8080 / 8444Guests reach it over IPv6; its limits count each guest’s full address.

A client-facing container can also open connections out over IPv6 when a name it dials has an AAAA record (an LDAP server, a syslog collector, the push service). Docker sends them out with the host’s address. Which path is used is decided by your DNS.

Every limit that counts attempts per client counts an IPv6 client by its /64, because a host walking through the addresses of its own /64 would otherwise never reach a limit:

  • the API rate limits (login, token refresh, password reset, MFA challenge and verify, EST enrollment);
  • the login CAPTCHA guard;
  • the EST and SCEP enrollment guard: a blocked /64 is listed and unblocked as such;
  • the MFA service’s signature ban.

An IPv4 client is counted by its address, and an IPv4 client of a dual-stack socket (::ffff:a.b.c.d) is the IPv4 client. Logs, the audit trail and sessions always keep the full address.

The captive portal is the exception: its limits count a guest’s full IPv6 address, because on the portal’s LAN one /64 is the whole guest VLAN.

“Login from a new IP” means a new network. The USER_LOGIN_NEW_IP audit entry and the email to the user (email.login_new_ip_notify_enabled, on by default) are written when a login comes from another /64 or another IPv4 address than the previous one. An IPv6 host that changes address inside its /64 is not a new IP.

IPv6 to the client-facing ports — 80/443, MFA, TACACS+, RADIUS, NAC, EST/SCEP and the captive portal — is forwarded by Docker into the container, exactly as IPv4 always is. Rules in the host’s INPUT chain, and therefore ufw, do not see that traffic. Filter it in Docker’s DOCKER-USER chain instead (Docker’s guide: Packet filtering and firewalls). For example, accept RADIUS over IPv6 only from your management prefix:

Terminal window
sudo ip6tables -I DOCKER-USER -p udp -m conntrack --ctorigdstport 1812 ! -s 2001:db8:10::/48 -j DROP

The same rule written with iptables and an IPv4 network covers IPv4. Match the published port with --ctorigdstport, as above: by the time a packet reaches DOCKER-USER its destination has already been rewritten to the container. Rules added this way are not kept across a reboot unless your host saves its ip6tables and iptables rules.

The same applies to port 8446 (plain-HTTP SCEP), which is published on every installation whether SCEP is on or not — see SCEP → Ports and firewall.

The HA ports 5432, 8008 and 2379 belong to containers kept off IPv6, so IPv6 to them still passes the host’s INPUT chain and ufw. The HA host-firewall rule stays as it is — see The node’s own Docker network.

Docker’s first IPv6 network turns IPv6 forwarding on, and the kernel then ignores router advertisements on interfaces with accept_ra=1. A host that takes its IPv6 address or its default route from advertisements would lose that route when the current one expires, hours later.

install.sh and ./taranac update therefore set accept_ra=2 on exactly the interfaces that have accept_ra=1 and a dynamic global IPv6 address or a default route learned from an advertisement, keep it in /etc/sysctl.d/99-taranac-accept-ra.conf, and say so. A host with a static IPv6 configuration, or with no IPv6 at all, is left as it is.

The change needs root: they run as root, use sudo without a password, or let sudo ask at a terminal. Without any of these they stop with nothing changed and print the commands, one echo '<key> = 2' | sudo tee -a /etc/sysctl.d/99-taranac-accept-ra.conf line per interface; run those once, then the same command again.

With "ip6tables": false in Docker’s daemon configuration (or --ip6tables=false), Docker cannot forward IPv6 into the containers. Its userland proxy takes the connection on the host instead, and every IPv6 client arrives from the gateway address 10.255.255.225. Devices are then matched, logged and blocked by that one address. install.sh and the update warn about it. To give IPv6 clients their own address, remove the setting, restart Docker (sudo systemctl restart docker) and run ./taranac up -d.

A TACACS+ connection from the gateway address is logged as The source address was rewritten by Docker and raises no alert. Never add the gateway to the inventory: every client the proxy forwards would arrive from it and be let in with its key. See A rewritten source address.

Inventory entries that covered the gateway

Section titled “Inventory entries that covered the gateway”

An inventory entry that covers the Docker gateway — the stack’s subnet, a /8, 0.0.0.0/0 — matches IPv6 devices only while they arrive from the gateway address. With their own address they match the IPv6 entries alone and are refused as unknown without one.

./taranac update and install.sh run on an existing installation look for such entries before they change anything. While such entries exist and the inventory has no IPv6 entry at all (no IPv6 address, prefix or range with TACACS+, RADIUS or NAC on), they name the entries and stop: add an IPv6 entry beside each with the same key — the devices’ IPv6 addresses or prefix, or ::/0 for all of them — and run the command again. With an IPv6 entry in the inventory they name the entries and go on. To go on regardless, answer CONTINUE at the prompt, or pass --ipv6-own-address-ok (./taranac update, ./install.sh) or set TARANAC_IPV6_OWN_ADDRESS_OK=1; IPv6 devices are refused until they have entries.

./taranac diagnose names those entries too: as a finding while IPv6 devices still arrive from the gateway, otherwise in its ipv6.txt.

./taranac diagnose --protocol <tacacs|radius|nac> --device <address> writes ipv6.txt into its archive: Docker’s version and whether its ip6tables is on, kernel IPv6, IPv6 forwarding, accept_ra per interface, whether the stack network carries IPv6, and for each published port of the daemon its IPv6 listener and Docker’s IPv6 DNAT rule (the DNAT part needs root or sudo). Problems found there appear in the summary marked [IPv6]. See Diagnostic bundles.

SymptomCauseFix
IPv6 devices are logged or refused with the address 10.255.255.225Docker’s ip6tables is off, so the userland proxy forwards IPv6Turn ip6tables back on, restart Docker, ./taranac up -d. ipv6.txt shows docker_ip6tables: OFF.
An IPv6 device is refused as Not in the inventoryNo entry covers its IPv6 addressAdd an IPv6 entry with the device’s key. See IPv6 devices.
An IPv6 RADIUS device gets no answerNo RADIUS-enabled entry covers the address the packets come fromRegister that IPv6 address or prefix.
ufw rules do not stop IPv6 to a service portPublished ports are forwarded, not delivered to INPUTFilter in DOCKER-USER, above.
The host loses its IPv6 default route hours after installingaccept_ra=1 on an interface that lives on router advertisements, not covered by the change aboveSet accept_ra=2 on it and persist it in /etc/sysctl.d/99-taranac-accept-ra.conf.
Install or update refuses: Docker … is too old, The docker command here is Podman, kernel has IPv6 turned off, or Docker refused to create a network with IPv6 beside IPv4The host fails a requirement aboveFollow the printed fix; nothing was changed.
A CoA to a switch registered by its IPv6 address failsCoA over IPv6 is not supportedRegister the switch’s IPv4 management address for CoA.