Ir al contenido

Standalone captive portal

A standalone captive portal runs on a host of its own, usually at a branch or in a DMZ segment, and serves its segment by itself. It hands out the addresses (DHCP), answers DNS and shows the sign-in page. It reaches the core for two things only: the guest API, which carries registrations and BYOD logins, and a heartbeat. It holds no database. Configure it in the console like any captive portal, then install it with one command on the host.

Unlike an embedded portal, a standalone portal can open the network itself. It then routes its segment and lets a signed-in guest out through its own firewall, with no switch or access point asked for anything.

TermMeaning
BoxOne installed portal: the container on one host. One portal can have several boxes, each reported under Portal Instances.
Who opens the networkThe portal opens access (the box routes the segment and admits guests in its own datapath) or The network is asked again (a CoA asks the switch or access point to re-authorize the device). Chosen on the Deployment tab.
DatapathWhat a box that opens access itself enforces: the firewall, per-guest speed limits, NAT and client isolation.
Guest-facing interfaceThe network card that faces the guests. DHCP, DNS and the sign-in page live there, and its address is the guests’ gateway.
Way outThe uplink towards the rest of the network. Only a box that opens access itself has one.
Portal tokenThe box’s standing credential for the core, written to its .env. Not a one-time code.
TrustHow the box verifies the core’s certificate: pinned on first contact, a CA file, or not at all.

After a guest signs in, the network has to let them out, and the Deployment tab asks After a guest signs in who does that.

The same sign-in, two answers. Left: the box routes the segment, so it adds the device to its allowed set, traffic starts to flow, the captive-portal API answers {"captive": false}, and the window closes without the guest ever being disconnected. Right: the device joins the flow’s target group, a CoA goes to the switch or access point, and the policy returns a new VLAN, so the station drops and reconnects there.

The portal opens access (recommended for Wi-Fi). The box is the guests’ DHCP server and default route. A device that has not signed in reaches the portal and nothing else. When the guest signs in, the box adds the device to its allowed list on its next report and lets its traffic out, and nothing else changes: the device keeps its address and its connection. Nothing is asked of a switch, so this works where switches cannot do 802.1X or dynamic VLANs. Specifically:

  • No CoA is sent, ever. A Disconnect-Request would throw the guest off the Wi-Fi a second after they were told they are online.
  • Sessions the box admits are listed in NAC Sessions with a Portal badge, the portal and the reporting box instead of a NAS, and an Admitted by the portal event instead of an Accounting-Start.
  • The box enforces speed limits, reachable destinations, NAT and isolation.

The network is asked again. The guest’s session is re-authenticated and the policy rules are read again. The switch or access point then applies whatever profile comes back. A new ACL goes unnoticed, but a new VLAN disconnects the guest, and on Wi-Fi there is no way to tell a station to reconnect the way a wired port can be bounced. The box still serves DHCP and DNS on the registration VLAN, with 30-second leases so a moved device soon asks for an address in its new VLAN, but it does not route.

A new standalone portal starts with The portal opens access proposed. An embedded portal can only use the second answer.

The Deployment tab draws these. Which one you are building decides the answer above.

ShapeWhat happensAccess held by
No NAC at allGuests connect to an access point or an unmanaged switch with no authentication. The box routes the segment and is the only gate.Gateway
MAB, portal opens accessA switch or access point authenticates the device by MAB (so it has an endpoint and a NAC session), and the box decides what the device may reach after sign-in.NAC + Gateway. Revoking acts at both ends.
MAB, network asked againMAB places the device in a registration VLAN. After sign-in a CoA re-authorizes it, and the policy returns a new ACL (nothing felt) or a new VLAN (the device reconnects).NAC

Cisco and Aruba both advise against changing a Wi-Fi client’s VLAN after it has associated, for the reason above. Where guests are on Wi-Fi, prefer one of the first two shapes.

Where the box opens access, the guest stays on the sign-in page, which counts down and then loads either the Redirect After URL or the portal’s own page. Where the network opens access, the page says in advance that the device is being moved and the window may close by itself, and asks the guest not to press Cancel, which on iOS disconnects the device instead of closing the window. See What the guest sees.

A phone’s sign-in window checks the network again only after a full page load, or on its own timer, which runs in minutes. A box that opens access itself therefore also publishes the captive-portal API of RFC 8908. It advertises the API’s address in DHCP option 114 (RFC 8910), and the moment it lets a guest through, the API answers {"captive": false}. iOS 14+, Android 11+ and ChromeOS ask this API, so the window goes away as soon as access is open.

The option is published only when all of the following hold:

  • the portal opens access itself;
  • its Portal URL is https:// with a host name, not an IP address. RFC 8910 advises against an address, and phones will not accept one;
  • that host name has a certificate a phone can verify, placed on the host.

The box answers its own host name with its own address inside the segment, and answers every other name truthfully. That is how a guest’s phone reaches the page, and the API, over TLS with a certificate that matches. Without a proper name, the box publishes no option at all rather than one every phone rejects without saying why. The window then closes when the page reloads itself after sign-in, or on the phone’s own timer.

  1. Create the portal under NAC → Endpoints → Captive Portals → New portal with Deployment Mode set to Standalone (DMZ), give it flows with target groups, and save. It opens again on its Deployment tab.
  2. Choose who opens the network on that tab and, if the portal does, what it enforces. Save.
  3. Generate a token. The install command is shown before a token exists, with a placeholder where the token will go, so you can read what it will do before you do anything. Generate New Token fills it in. The token is shown once; copy it then. When Backend API URL on the General tab is empty, generating the token fills it with the address you reached the core at. Review it and save the portal.
  4. Run the command on the portal host, from the directory where the Taranac distribution is unpacked:
Terminal window
./taranac captive-portal join --core https://core.example --token <portal-token> --trust-any

Change the URL if the host reaches the core under a different name (NAT, a public name, another port). Whatever name you use must be in the core certificate’s SANs; a core reached by IP needs an IP SAN. The line includes --trust-any on purpose, as explained under Trust in the core.

On a Taranac virtual appliance, run sudo taranac-module portal instead. It asks for the same two values, the URL and the token, and installs from images already on the disk.

The installer fetches the portal’s configuration from the core with the token, then asks about this host. These questions are asked on the host because the console has never seen the machine, and a wrong answer there would give a portal that saves cleanly, reports healthy and serves nobody.

QuestionAsked whenProposed answerFlag
Which interface faces the guests? It must exist and hold an IPv4 address, which becomes the guests’ gateway and the address DHCP, DNS and the sign-in page listen on.AlwaysThe one the previous install used, otherwise the interface it can identify as the guests’ side--client-interface <if>
Which interface is the way out? It must differ from the guests’ side.The portal opens accessThe interface of the default route--uplink-interface <if>
Which addresses should the guests get? Both ends must be inside the guests’ interface network, in order.AlwaysThe previous install’s range, otherwise .100–.200 of the interface’s network--dhcp-range "first last"
Which resolver should the portal ask for names it does not answer itself?AlwaysThe resolver this host uses, otherwise 1.1.1.1--upstream-dns <ip>
Masquerade guest traffic behind the uplink’s address? Off only for a routed segment with real addresses.The portal opens accessYes--nat on|off
Stop guests on this segment from reaching each other?The portal opens accessYes--client-isolation on|off

It shows the interfaces it can see, proposes an answer and judges each answer as it is given. An interface that does not exist, a guests’ interface without an address, the same interface on both sides, or a range outside the network is refused rather than guessed. After three wrong answers it stops, installs nothing and says which flag to use. A reinstall offers the previous answers as defaults. With flags, or with -y / --yes to accept every proposal, it asks nothing; a flag with a wrong value is refused, not corrected.

It then prints a summary (guests’ interface, addresses, resolver and, for a box that opens access, the way out, NAT and isolation) and asks to continue. A box that opens access also needs the host prepared once: IPv4 forwarding on and the traffic-shaping and ipset kernel modules loaded, both kept across reboots. The installer does this itself, which needs sudo. A firewall of your own on the host is left as it is and pointed out.

Finally it starts the portal and waits for the first accepted heartbeat, which is the only proof that token, trust and route all work, before reporting success. If none arrives, it prints the reason from the portal’s log and exits with an error. The box then appears under Portal Instances.

The portal is installed in a captive-portal directory beside the distribution (--dir to change it), with its own docker-compose.yml, .env (mode 0600, holding the token) and certs/. The container uses the host network: it binds 80 and 443 for the portal, 53 for DNS and 67 for DHCP.

The channel from the portal to the core carries BYOD usernames and passwords, so the box verifies the core’s certificate in one of three ways:

TrustAsked for withWhat it does
pinnothing (the default)Trust on first use: the chain the core presents on first contact is the only one accepted afterwards.
ca--ca-file <path>Verify against a CA bundle you supply. Behind a reverse proxy, that is the proxy’s CA.
any--trust-anyVerify nothing.

The console’s command includes --trust-any because a fresh host has no reason to trust the core’s certificate yet, and an install that stops there tends to get finished with the flag anyway, then kept. Each box reports the trust it ended up with as Verifies this core in its instance details, so a box that verifies nothing is visible. For a production site, remove the flag from the line (the box then pins) or replace it with --ca-file.

If a pinned core certificate changes, the box stops and prints both fingerprints rather than adopt the new one. When you know the certificate was replaced, re-pin with ./taranac captive-portal reset-trust; no new token is needed. If it was not replaced, something else is answering for the core, so do not clear the pin.

A saved standalone portal has a Deployment tab, from top to bottom:

  • After a guest signs in: who opens the network, with the three shapes drawn out (How this works).
  • What this portal enforces: shown when the portal opens access. See below.
  • API Token: the current token’s prefix, What this box will be handed (who opens the network, DHCP and DNS served by this box, the default speeds, report interval and idle timeout), the install command and Generate New Token. The wiring is not here: the installer asks the host.
  • Portal Instances: every installed box. See below.
  • Gateway status: what each box that opens access is enforcing right now. See below.
FieldNotes
Default download / Default uploadPer-guest speed limit in kbit/s. 0 means no limit. The lowest of three levels: a guest session’s own limit wins, then the flow’s (Speed limit for this flow on the Flows tab; empty uses this default), then this.
Reachable before sign-in (Advanced)IPs or CIDR ranges an unauthorized client may reach. The portal, DHCP and DNS are always reachable. Host names cannot be used, because the only resolver in the segment is the box itself.
Never reachable (Advanced)IPs or CIDR ranges no client may reach, even after signing in. This is where a guest network is kept away from the corporate one.
Always allowed (endpoint groups) (Advanced)Devices in these groups pass without the portal: a printer, a camera or a payment terminal has no browser to sign in with. A blocked endpoint is still refused.
Idle timeout (Advanced)Ends a session early when the device has sent no traffic for this many minutes, which frees its shaper and firewall entries. It is not how long a guest may stay; that is the flow’s session duration. 0 disables it.
Report interval (Advanced)How often the box picks up changes, 1–60 seconds (3 by default). This is also how long a guest waits between being granted access and having it.

Saved settings reach a running box on its next report, with no redeployment or restart.

One row per installed box: Name, IP, Version, Segment (the guests’ interface and the range it hands out), Connected to node, Status, Last Seen. Boxes send a heartbeat every 30 seconds. A box is online if the last one arrived under 60 seconds ago, degraded from 60 to 300 seconds, and offline after that. A box silent for 7 days is removed. The portal’s badge in the list rolls these up.

Connected to node names the cluster node that received the box’s last heartbeat. It comes from the core’s side, not from the box’s configuration, so a box pointed at a different node shows the new node on its next heartbeat. Under HA, each box connects to exactly one node, and an outage of that node is what silences it.

The What this box reports button (information icon) opens what the box reported about itself, read from the host rather than from what the console proposed:

SectionFields
BuildImage, Built from release, After sign-in, Verifies this core
How it is wiredFaces the guests, Way out, NAT, Client isolation
Addresses it hands outDHCP, Range, Mask, Gateway given to guests, Lease time, DNS, Upstream resolver, Reports its leases
ContactReached the core from, Portal address, Last heartbeat

A value that differs from what you expected is a fact, not an error: the installer asked on that machine. To change the wiring, run the install command again.

For a portal that opens access, each box reports its datapath in its own words: when it last reported, badges for Firewall, NAT and Shaping, and Isolated or Isolated (routed only). The second means the box can isolate only traffic it routes; clients of the same access point or unmanaged switch are switched there and never reach the box, so isolation between them is that device’s own setting (client isolation on OpenWrt, default-forwarding=no on RouterOS). Below the badges, the counts of devices seen, allowed through and waiting for an address, and anything the box reports as not working yet.

The clients table lists every device on the segment, allowed ones first, with Device, Address, Who (allowed or not allowed through), Speed (down / up) and Traffic, and a filter by MAC, address or name. A box whose heartbeat arrives but whose datapath has never reported is called out: its page loads, but nothing is enforced.

A standalone portal is the DHCP server of its segment, so it reports every lease it grants, including the address it actually handed out. The lease fills the endpoint’s IP address, which a device authenticated by MAB would otherwise lack. The lease and RADIUS accounting rank equally: the newer first-hand report wins, and an old lease never overwrites newer accounting. The lease is also DHCP evidence for Device Profiling. It appears in the DHCP Probe as Served, and the box as a Captive portal listening point.

What the portal presents to a guest’s phone must be publicly trusted. A certificate from your own CA, including Taranac’s NAC PKI, causes the same browser warning as a self-signed one. Put the certificate on the host:

  • certs/portal.crt: the full chain, your certificate first, then its issuers;
  • certs/portal.key: its private key, without a passphrase (mode 0600).

The name on the certificate must be the Portal URL’s host name, since that is where guests are sent. Without these files, the container generates a self-signed pair on first start: the page works, but every guest’s browser warns. A certificate already there is kept. Keep a copy elsewhere, because uninstalling deletes this directory.

After replacing the files, run ./taranac captive-portal restart portal. The certificate is checked before anything is stopped, so a mistake costs a refused command and the running portal keeps serving guests. The same check runs whenever the container starts, and nginx is not started with a certificate it cannot serve.

The check refusesThe check warns about
A missing portal.crt or portal.keyCertificate blocks that could not be read and were ignored
A key protected by a passphrase, or not PEM (DER and PKCS#12 must be converted first)A certificate that has expired, or expires within two weeks
A file with no certificate blockAn issuer missing from the file (incomplete chain)
A file in which no certificate belongs to the keyA name guests are sent to that the certificate does not cover
A first certificate that is not yours. It tells an indented PEM header (OpenSSL skips that block without a word, so the next certificate is served) apart from a chain in the wrong order

The portal is addressed by name through the taranac wrapper beside the installer, so no command needs to be run from inside its directory:

CommandWhat it does
./taranac captive-portal join --core <url> --token <token>Install, or reinstall, the portal on this host. Takes the flags from What the installer asks, plus --ca-file, --trust-any and --dir.
./taranac captive-portal statusTrust, token, heartbeat and datapath, read from local state, so it works while the core is unreachable.
./taranac captive-portal datapathThe firewall rules, the allowed clients and the speed limits in force right now, read from inside the container.
./taranac captive-portal restart portalCheck the certificate in certs/, then restart and wait until nginx answers. Refuses if the certificate cannot be served.
./taranac captive-portal update [--check]Move the portal to the release its core runs. The address, token and wiring are already in the install and are not asked again. If the new version does not reach the core, the install is rolled back. --check reports without changing anything.
./taranac captive-portal reset-trustAccept the core’s certificate again after a real replacement.
./taranac captive-portal uninstallRemove the portal, its datapath and its directory from this host. The portal still exists on the core; delete it there as well.
./taranac captive-portal logs -f portal, ps, …Anything else is passed to docker compose in the portal’s directory.

A portal runs the same release as its core. After upgrading the core, run update on each portal host in the same window. See Versioning.

Guest Wi-Fi at a branch with no NAC. Access points on an unmanaged switch, one box with two network cards. The portal opens access, NAT and isolation are on, and Never reachable lists the corporate ranges. Sessions show Gateway.

Guest Wi-Fi behind MAB. Access points authenticate by MAB to Taranac and put guests in a guest VLAN routed through the box. The portal opens access, so the guest is never moved. Sessions show NAC + Gateway, and revoking disconnects at both ends.

Wired registration VLAN. Switches place unknown devices in a registration VLAN served by the box. The network opens access: a CoA bounces the port where the switch supports it, and the device takes a new address in its production VLAN.