pfSense Integration
Integrating Captivo with the pfSense Captive Portal
1Prepare on the Captivo side
Captivo only answers RADIUS requests from devices it knows. Open Settings -> RADIUS in the Captivo panel and add your pfSense:
- NAS IP Address: the address pfSense reaches us from - its WAN / public IP, not its LAN address.
- Shared Secret: you will enter the same value on the pfSense side in step 2.
- Auth port: 1812 · Accounting port: 1813
While you are here, download the portal page as well: Settings → Portal, choose pfSense as the platform and download the template. You will upload it in step 4, and having it ready means the whole zone page can be filled in one pass.
2Define the Captivo RADIUS server
System → User Manager → Authentication Servers → Add. This is defined once here and then selected by the captive portal zone.
| Field | Value |
|---|---|
| Descriptive name | Captivo |
| Type | RADIUS |
| Protocol | MS-CHAPv2 (recommended). PAP also works, but MS-CHAPv2 does not put the password on the wire. |
| Hostname or IP address | radius.captivo.io |
| Shared Secret | the value from step 1 |
| Services offered | Authentication and Accounting |
| Authentication / Accounting port | 1812 / 1813 |
| RADIUS NAS IP Attribute | your guest-facing interface (e.g. LAN) |
radius.captivo.io, enter your own server's address or IP - the machine the Captivo RADIUS container runs on. Ports 1812/1813 UDP must be reachable from pfSense.3Create the Captive Portal zone
Services → Captive Portal → Add. Give the zone a name (captivo in the screenshots), then on the Configuration tab:
- Enable Captive Portal - ticked
- Interfaces - the interface guests connect to (LAN in the screenshot). Never the WAN: enabling the portal on the WAN puts a login page in front of your own internet connection.
- Idle timeout / Hard timeout - optional. Leave them blank if Captivo profiles already set a session duration; pfSense would otherwise cut the session at whichever limit comes first.
Further down the same tab there is one more setting that matters: Per-user bandwidth restriction. Tick Enable per-user bandwidth restriction and leave Default download and Default upload EMPTY.
4Upload the portal page
- 1. On the same zone page, scroll to HTML Page Contents → Portal page contents and upload the template you downloaded in step 1.
- 2. Upload the error page to Auth error page contents as well. It is not optional: with RADIUS MAC authentication enabled, this is the FIRST page a guest whose device is not on the whitelist ever sees, so a guest who cannot register there is stuck. The downloaded error page carries every sign-in method the portal page does.
- 3. Do not save yet - carry on down the page to Authentication and Accounting, then save once at the bottom.
5Authentication
Still on the same page, scroll down to the Authentication block.
- Authentication Method:
Use an Authentication backend- this is the option that shows the login page and authenticates against RADIUS. ("None" lets anyone through with one click; "RADIUS MAC Authentication" authenticates devices by MAC instead, and shows the login page only if Login page Fallback is ticked - see step 8.) - Authentication Server: Captivo
- Secondary authentication Server: leave empty unless you also want pfSense's own local accounts to work; a second server adds a second set of input fields to the login page.
- Session timeout:
Use RADIUS Session-Timeout attributes- required for Captivo session durations to apply. - Traffic quota:
Use RADIUS pfSense-Max-Total-Octets attribute- required for data quotas. - Per-user bandwidth restrictions:
Use RADIUS pfSense-Bandwidth-Max-Up / -Down attributes- required for speed limits, together with the master switch in step 3. Captivo sends the WISPr attributes, which this same option accepts. If "Enable per-user bandwidth restriction" is off, ticking this alone changes nothing.
NAS Identifier (optional, extra isolation)
Captivo can additionally verify that a request really comes from your firewall. In the Captivo panel go to Settings → RADIUS → NAS-Identifier Verification, press Enable and copy the generated value (e.g. captivo-yourhotel-a8f9c2d1) into the NAS Identifier field here.
Order matters: paste it into pfSense first, then enable verification in Captivo. If verification is on while the field is empty, pfSense sends its default (CaptivePortal-<zone>), Captivo rejects it and every login fails.
6Accounting - the two settings that matter
Authentication only tells Captivo who logged in. Everything else - live sessions, duration, data usage, reports - comes from accounting.
- Send RADIUS accounting packets: ticked.
- Accounting Server: Captivo
- Send accounting updates:
InterimCaptivo treats a session as live while the firewall keeps signalling it. "Interim" sends an update every minute. With "No updates" or "Stop/Start", pfSense reports only the start and the end - so a guest who is still online disappears from Active Sessions after 10 minutes, while pfSense's own status page still lists them. If your Active Sessions list is almost always empty, this is why. - Accounting style:
Invert Acct-Input-Octets and Acct-Output-Octets- ticked.Without this, download and upload arrive the wrong way round and a guest appears to have uploaded far more than they downloaded. Note that pfSense's own description of this option reads as though it would cause the swap rather than fix it; the setting shown here is the one verified to produce correct figures in Captivo. - MAC address format:
Default- any format works; Captivo normalises it.
7Walled garden (Allowed Hostnames)
The guest is not online yet when the portal page opens, so the few addresses the page needs must be reachable before login. Without this the portal looks broken: the page opens but sending an SMS code or submitting the form does nothing.
Services → Captive Portal → your zone → Allowed Hostnames → Add, with Direction = Both for each:
app.captivo.io- the portal API the page talks tocaptivo.ioradius.captivo.io
The walled garden alone is not enough
Two independent layers stand between a guest and the Captivo server, and traffic passes only if both allow it. The walled garden keeps an unauthenticated client from being caught by the portal redirect when it goes to that address. Whether the packet reaches the address at all is decided by the firewall rules on the guest interface.
With only the first layer in place the symptom is misleading: the login page opens - it is hosted on pfSense itself and needs no access to Captivo - but submitting the form does nothing, because that request is the first one that has to reach the Captivo server. A page that opens is not proof that access exists.
The rule belongs on the guest interface, above the rules that block guest traffic:
- Source — the guest VLAN
- Destination — the Captivo server address
- Port — TCP 3000, or TCP 80 and 443 if your deployment runs Caddy
- Action — pass
Which ports apply depends on your deployment, and opening the wrong ones does not make it safer - it only widens the rule for no benefit. This command shows what is actually listening:
docker compose -f docker-compose.onprem.yml --env-file .env.onprem ps
If the output shows 80 and 443, Caddy is in use and you do not need to open 3000. If it does not, the web service listens only on 3000 and 80/443 belong to no service at all.
More than one guest VLAN
A Captive Portal zone can cover several interfaces. Adding a VLAN to the zone makes the portal appear on that VLAN - it does not give that VLAN access to the Captivo server. In pfSense firewall rules are per interface, and the zone setting does not create them.
So the rule above has to exist for every guest VLAN. Rather than repeating it, create a group under Interfaces -> Interface Groups containing the guest VLANs and write the rule once on the group; a group rule applies to every interface in it.
8Devices that should skip the portal
Printers, smart TVs, payment terminals and cameras cannot fill in a login page. On pfSense, use the portal's own bypass list for these:
Services → Captive Portal → your zone → MACs tab → Add, action Pass, and the device's MAC address.
Devices passed through this way never authenticate against Captivo, so they do not appear in Active Sessions, guest records or reports. Their traffic is still captured by the Log Server if you forward pfSense's firewall logs there. To manage the list from Captivo instead - and to see those devices in sessions and reports - use the RADIUS route below.
Or: manage the list from Captivo (RADIUS MAC Authentication)
Services → Captive Portal → your zone → Authentication:
- Authentication Method:
Use RADIUS MAC Authentication- devices are authenticated by MAC address against Captivo's MAC Whitelist. - Authentication Server: the Captivo server from step 2.
- RADIUS MAC Secret: must NOT be empty. pfSense sends the MAC address as the username and this value as the password. Captivo does not check the password at all - any value works - but if you leave the field blank pfSense never sends the request and only writes "RADIUS MAC Authentication Failed" to its log. This is the single most common cause of "the whitelist does not work".
- Login page Fallback: tick it. Without it, a guest whose device is not on the whitelist is blocked outright and never sees the portal - MAC devices and portal guests could not share one zone. With it ticked they can.
- MAC address format:
Default- Captivo recognises every common spelling, so this can stay as it is.
If it still does not work: on the Captivo host, run docker exec captivo-postgres psql -U captivo -d captivo -c "SELECT authdate, username, reply FROM radpostauth ORDER BY authdate DESC LIMIT 20;" right after a test. No MAC-shaped username in the output means pfSense never asked - check RADIUS MAC Secret first.
Troubleshooting
The Active Sessions list is empty although guests are online
- Send accounting updates must be Interim (step 6). This is by far the most common cause.
- Check that Send RADIUS accounting packets is ticked and the Accounting Server is Captivo.
- Services offered on the RADIUS server must be Authentication and Accounting, not authentication only.
- UDP 1813 must be open on the path from pfSense to the Captivo RADIUS server.
Download and upload look swapped
- Tick Invert Acct-Input-Octets and Acct-Output-Octets under Accounting style (step 6).
- Only sessions that start after the change are affected; closed sessions keep their recorded figures.
The portal page opens but login always fails
- Shared secret mismatch between Captivo (Settings -> RADIUS) and pfSense (User Manager). Re-enter it on both sides.
- The NAS IP registered in Captivo must be the address pfSense actually reaches us from - its WAN/public IP, not its LAN address.
- If NAS-Identifier verification is enabled in Captivo, the NAS Identifier field in pfSense must contain exactly the generated value.
- UDP 1812 must be open on the path.
The page opens but the SMS code never arrives / the form does nothing
- This is the walled garden (step 7). The guest is not online yet, so the addresses the page calls must be allowed before login.
- Check the Allowed Hostnames entries and that Direction is Both.
- On-premise: allow your own server address, not captivo.io.
- The walled garden is only one of two layers. Even with the address allowed there, the guest interface needs a firewall rule permitting the guest VLAN to reach the Captivo server - see step 7. The page opens without that rule, because it is hosted on pfSense; only the form submission fails.
- With more than one guest VLAN, check that the rule exists on the VLAN the guest is actually on. Adding a VLAN to the zone does not create its firewall rule.
Speed limit / session duration / quota is not applied
- The three attribute options in step 5 must be ticked; otherwise pfSense receives the values and ignores them.
- Leave the zone's own Idle/Hard timeout blank if the duration should come from the Captivo profile - pfSense applies whichever limit is shorter.
For product-wide problems: The portal does not open · Sign-in is rejected · Sessions do not appear