DOCUMENTATION

pfSense Integration

Integrating Captivo with the pfSense Captive Portal

Requirements: pfSense CE 2.7 or newer, and an interface your guests connect to. The screenshots below are from a working installation. In pfSense 2.4/2.5 the Authentication tab looked different - the RADIUS server was entered on the captive portal page itself; from 2.6 onwards it is defined once under User Manager and then selected here.
Two settings decide whether Captivo shows correct data, and both are easy to miss: Send accounting updates = Interim and Accounting style = Invert Acct-Input-Octets and Acct-Output-Octets. Both are in step 6.

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.

A mismatched shared secret is the single most common cause of failure, and it does not produce a helpful error: pfSense simply says the login failed. If nothing works at the end, come back and re-enter the secret on both sides.

2Define the Captivo RADIUS server

System → User Manager → Authentication Servers → Add. This is defined once here and then selected by the captive portal zone.

FieldValue
Descriptive nameCaptivo
TypeRADIUS
ProtocolMS-CHAPv2 (recommended). PAP also works, but MS-CHAPv2 does not put the password on the wire.
Hostname or IP addressradius.captivo.io
Shared Secretthe value from step 1
Services offeredAuthentication and Accounting
Authentication / Accounting port1812 / 1813
RADIUS NAS IP Attributeyour guest-facing interface (e.g. LAN)
User Manager -> Authentication Servers: the Captivo RADIUS server. Services offered must be "Authentication and Accounting" - with authentication only, no session or usage data is ever produced.
On-premise: Instead of 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.
Services -> Captive Portal -> Configuration: the portal is enabled and bound to the guest interface. The MACs tab at the top is pfSense's own bypass list - see step 8.

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.

Per-user bandwidth restriction: the master switch for speed limits. pfSense's own note says "RADIUS servers can override the default settings" - so with this off, the speed in a Captivo profile has nothing to apply to.
Leaving the two default boxes empty is deliberate: a guest whose Captivo profile carries no speed limit then stays unlimited, instead of being capped at a number entered here. The per-user values from Captivo still apply. Step 5 has the second half of this - the RADIUS attribute option must be ticked as well.

4Upload the portal page

  1. 1. On the same zone page, scroll to HTML Page Contents → Portal page contents and upload the template you downloaded in step 1.
  2. 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. 3. Do not save yet - carry on down the page to Authentication and Accounting, then save once at the bottom.
HTML Page Contents: upload the template downloaded from Captivo as the portal page. "Live View" lets you check the result without connecting a guest device.
Re-download the template after changing portal settings in Captivo (login methods, texts, colours, header style). The file on pfSense is a snapshot; it does not update itself. The panel warns you when a change requires this.

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.
Authentication: backend = Captivo, and the three RADIUS attribute options ticked. Without them the profile you assign in Captivo (speed, duration, quota) is sent but silently ignored.

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: Interim
    Captivo 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.
Accounting: "Interim" and "Invert Acct-Input-Octets and Acct-Output-Octets". These two are the difference between a working dashboard and an empty one. Save at the bottom of the page.
Changing this affects new sessions only. Sessions that have already closed keep the figures recorded at the time.

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 to
  • captivo.io
  • radius.captivo.io
Allowed Hostnames: the addresses the portal page needs before the guest is authenticated.
Each entry is added with Direction = Both. Leave the bandwidth boxes empty.
On-premise: Do not add the captivo.io addresses - your installation does not use them. Add your own server's address (the hostname or IP the panel is served from) instead. If you reach the panel over plain IP, use the Allowed IP Addresses tab rather than Allowed Hostnames.

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.

A common way this goes wrong: a new VLAN is added to the zone, the portal appears on it, and it looks done - but guests on it can open the page and never get past the form, because the firewall rule exists only on the VLAN that was set up first. The portal appearing is the part that needs no rule.

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)

Note! This route replaces the zone's Authentication Method. Three settings must be correct together — miss any one and it fails in a way the error message does not explain. Verified end to end on 2026-09-04.

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.
The three settings that matter are boxed: the method itself, a non-empty RADIUS MAC Secret, and Login page Fallback. Leave Reauthenticate Users OFF - see the note below.
With this method the error page becomes the first page a guest whose device is not on the whitelist ever sees. Upload the downloaded template to Auth error page contents as well as the portal page (step 4) - otherwise that guest lands on a page where they cannot register.
Leave Reauthenticate Users unticked. It re-checks every logged-in device once a minute, and with a per-device limit in the guest profile that re-check is rejected as a second simultaneous session - disconnecting every guest a minute after they connect. Measured, not assumed.

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

Was this article helpful?