Captive Portal

Captive Portal

MikroTik HotSpot · Operator guide

Router setup guide

Configure a real MikroTik RouterOS HotSpot so guests land on the live portal, submit the login form (submit = consent), and get internet. The portal is already running — you only touch the router.

01 Before you start

This guide configures a MikroTik RouterOS HotSpot on your physical or virtual router. Guests on guest WiFi are redirected to the live captive portal, fill name/age/gender (Male / Female / Other), and submit (that submit is consent — there is no separate checkbox). Then they receive internet. You only touch the router — the portal handles the login page and guest records.

Split diagram: captive portal runs on your server; MikroTik HotSpot runs on your router
Left: already deployed on your server. Right: what you configure on the MikroTik today.

Confirm the portal first

The guest login page is already live at https://captive-portal.firozmahmud.dev/login. Open it in a browser and confirm the form loads before you change anything on the router.

Prerequisites — check each item as you confirm it

02 How it works

Follow the guest journey from WiFi join to internet access:

Network diagram: guest phone on WiFi connects through MikroTik HotSpot to the captive portal, then gains internet access after login
Guest traffic is intercepted by the router, sent to your portal for login/submit (consent), then released to the internet once authenticated.
  1. Join WiFi Guest connects and receives an IP from the router.
  2. Get redirected Any website visit sends the guest to /login with mac= and link-login-only.
  3. Submit form Guest enters name, age, gender (Male / Female / Other) and submits — submit = consent (no checkbox).
  4. Portal unlocks Portal saves the record and POSTs HotSpot credentials to $(link-login-only).
  5. Online Router opens internet for that MAC. Walk-away / idle ≈5m ends the HotSpot session (profile default); MAC cookie up to ~3 days.

RADIUS stays off

This guide uses MikroTik HotSpot with local users and the portal return-login POST — not an external RADIUS server. Leave RADIUS unchecked in the HotSpot wizard.

03 Rules for every router

Splash / login.html must pass the guest MAC

Without mac= the portal cannot unlock internet for the device.

Guests need a router intercept (or captive kick)

Typing bare HTTPS /login skips $(mac). Use the captive popup, any HTTP site, or /connect so HotSpot can inject MAC and unlock URL.

Walled garden must allow the portal only

Add captive-portal.firozmahmud.dev. Do not add the kick host (default neverssl.com) — HotSpot must intercept that HTTP URL.

HTTP PAP on the active server profile

POC: hotspot1 → profile hsprof4 (not only profile default). Missing http-pap causes login-page loops after submit.

Saving on the portal is not enough

The router must also receive the unlock POST with valid HotSpot credentials.

Session timers (verified POC)

Router user profile default: idle 5m, keepalive 2m, mac-cookie 3d. HotSpot profile hsprof4 must include http-pap. Portal splash/CSRF and auth TTL default 60 minutes.

What the router must send
ParameterMikroTikPurpose
Device MAC$(mac)Guest identity (required)
Unlock endpoint$(link-login-only)Portal POST target (required)
Router / bridge MACstatic router_mac in login.htmlAP identity (no $(router-mac))
Original site$(link-orig)Post-login redirect
Guest IP$(ip)Records (optional)
Identity / server$(identity), $(server-name)Site / HotSpot name

If MAC is missing

Guests see Could Not Identify This Device (or the form never gets a MAC). Open /connect or http://neverssl.com on guest WiFi — do not type the portal HTTPS URL expecting a redirect.

Line these up with portal settings: PORTAL_PUBLIC_URL for the domain and walled garden; MIKROTIK_HOTSPOT_USER / MIKROTIK_HOTSPOT_PASSWORD must match the HotSpot user in MikroTik configuration.

04 MikroTik Hotspot configuration

Real RouterOS settings — work through each step in WinBox or terminal. Expand a step, apply it on your router, then tick it off.

WinBox menu map showing IP, Hotspot, Server Profiles, Walled Garden, and Hotspot Users paths used in this guide
WinBox menu paths referenced in the steps below. Terminal equivalents are shown where useful.

Configuration progress: 0 / 6 steps complete

1 Enable Hotspot

WinBox IP → Hotspot → Hotspot Setup

  • Where: IP → Hotspot → run the setup wizard, or configure manually
  • Select the guest WiFi interface or bridge — never the WAN or management LAN
  • Leave RADIUS unchecked — authentication is local HotSpot users plus the portal POST

Verify

/ip hotspot print lists a hotspot server bound to the guest interface.

Terminal
/ip hotspot print
2 External login page

WinBox IP → Hotspot → Server Profiles → Login

Set Login By to HTTP PAP on the active server profile (e.g. hsprof4), then replace hotspot/login.html under WinBox Files → hotspot (not Files root):

HTML
<html><head><title>Redirect</title></head><body>
<form name="redirect" action="https://captive-portal.firozmahmud.dev/login" method="get">
  <input type="hidden" name="mac" value="$(mac)">
  <input type="hidden" name="ip" value="$(ip)">
  <input type="hidden" name="host-ip" value="$(host-ip)">
  <input type="hidden" name="hostname" value="$(hostname)">
  <input type="hidden" name="link-login-only" value="$(link-login-only)">
  <input type="hidden" name="link-orig" value="$(link-orig)">
  <input type="hidden" name="server-name" value="$(server-name)">
  <input type="hidden" name="identity" value="$(identity)">
  <input type="hidden" name="server-address" value="$(server-address)">
  <input type="hidden" name="interface-name" value="$(interface-name)">
  <!-- Per-router bridge MAC (no $(router-mac)). POC: e4:8d:8c:36:45:db -->
  <input type="hidden" name="router_mac" value="e4:8d:8c:36:45:db">
  <input type="hidden" name="ssid" value="captive-portal-open-wifi">
</form>
<script>document.redirect.submit();</script>
</body></html>

RouterOS substitutes $(mac), $(link-login-only), etc. with real values when it serves this page. Confirm path with /file print where name~"login.html" → must show hotspot/login.html.

RouterOS
/ip hotspot print detail
/ip hotspot profile set hsprof4 login-by=http-pap,http-chap,cookie
/ip hotspot user profile set [find name=default] idle-timeout=5m keepalive-timeout=2m mac-cookie-timeout=3d

Login By must be HTTP PAP on the active profile

MAC-based or CHAP-only on the wrong profile will not present this page / unlock. Fix the profile from /ip hotspot print detail (e.g. hsprof4), not only default.

3 Splash URL (reference)

Preferred hotspot/login.html (step 2) · optional profile Login page URL

Preferred path is the login.html form in step 2 (includes static router_mac). This query string is the same parameters for reference — or paste it if your profile uses an external login URL instead of the HTML file:

URL
https://captive-portal.firozmahmud.dev/login?mac=$(mac)&ip=$(ip)&host-ip=$(host-ip)&hostname=$(hostname)&link-login-only=$(link-login-only)&link-orig=$(link-orig)&server-name=$(server-name)&identity=$(identity)&server-address=$(server-address)&interface-name=$(interface-name)

Add static &router_mac=… and &ssid=… when using a profile URL alone (MikroTik has no $(router-mac)).

4 Walled garden

WinBox IP → Hotspot → Walled Garden

Allow the portal domain before login — guests have no other internet until authenticated.

RouterOS
/ip hotspot walled-garden add dst-host=captive-portal.firozmahmud.dev

Do not walled-garden the kick host

Leave neverssl.com (default CAPTIVE_KICK_URL) out of the walled garden so HotSpot can intercept that HTTP request and send guests back with mac=.

Verify

/ip hotspot walled-garden print shows the rule. From an unauthenticated guest device, captive-portal.firozmahmud.dev loads over HTTPS while other sites do not.

5 Shared HotSpot user

WinBox IP → Hotspot → Hotspot Users

After the guest submits the form, the portal authenticates to this HotSpot user on your router — not the guest's own credentials.

PortalMikroTik
portal-guest (env MIKROTIK_HOTSPOT_USER)/ip hotspot user name
MIKROTIK_HOTSPOT_PASSWORDsame password on the router
RouterOS
/ip hotspot user add name=portal-guest password=<portal-password> profile=default
/ip hotspot user profile set [find default] shared-users=100

Security

Use a strong, unique password — a shared secret between the router and portal, not something a guest sees. Never reuse the router admin password.

6 Test end-to-end

Verify Guest phone + portal guest list + terminal

  • Connect a phone to guest WiFi with mobile data off
  • Open any HTTP site (e.g. http://neverssl.com) — do not type the portal HTTPS URL expecting HotSpot to inject mac=
  • If you dismissed the captive popup: open /connect (portal kick → router intercept → /login?mac=…)
  • Confirm the address bar has mac=, link-login-only=, and (from login.html) router_mac=
  • Fill name, age, gender (Male / Female / Other) and submit (submit = consent — no checkbox) — ordinary browsing should work immediately

HTTPS-only browsing

https://google.com (and similar) cannot be redirected by HotSpot. Use HTTP, the captive popup, or /connect.

Verify

On the router: /ip hotspot active print lists the guest MAC. On the portal: the guest appears on /list immediately after submit (Connection status starts Offline until presence reports that MAC — see Presence). Admin /list updates live via SSE.

05 After the guest submits

Sequence diagram: guest browser submits form to portal, portal saves record, browser POSTs HotSpot credentials to router, then guest reaches the internet
The unlock POST runs in the guest browser to the router's private HotSpot login URL.

Submitting the login form is consent (hidden consent=1; no separate tick). The portal saves the guest record, then auto-POSTs the HotSpot username and password to link-login-only (the router's login endpoint) so the browser — not a server-to-server call — performs the unlock.

Portal POSTs to $(link-login-only) using HotSpot user portal-guest — password must match portal config (MIKROTIK_HOTSPOT_PASSWORD).

Unlock host safety

By default the portal only sends the HotSpot password to a private, loopback, or link-local IP (e.g. 192.168.190.1). A non-private link-login-only host is refused unless added to MIKROTIK_UNLOCK_HOSTS.

Operator pages

Guests (/list) shows consented records. API call history shows which portal routes were hit (by date). Neither changes unlock.

06 Presence heartbeat (verified on POC)

Near-realtime Online / Offline on /list. Does not change guest login or unlock — splash → form → browser POST to router /login stays the same.

Verified POC (Sep 2026)

On the live RB2011 this is verified: script portal-presence, profile default On Login + On Logout, idle-timeout=5m, schedulers portal-presence-startup + portal-presence-5m (walk-away → Offline ≈5m). Walled garden allows captive-portal.firozmahmud.dev. Steps below are for a new router or reinstall.

Recommended trigger

Run on HotSpot On Login and On Logout, plus scheduler portal-presence-5m and optional startup. Profile idle-timeout=5m covers walk-away.

  1. Portal secret Set the same value in portal .env / GitHub secret MIKROTIK_PRESENCE_SECRET and inside the MikroTik script. Compose must pass it into the portal container; redeploy. Empty secret → API returns 503.
  2. System → Scripts → New Name exactly portal-presence (not script1). Check Don’t Require Permissions. Paste source from repo setup/mikrotik/presence-report.rsc (or gitignored presence-report.local.rsc with secret already filled).
  3. Edit CONFIG POC defaults already match: SSID captive-portal-open-wifi, bridge bridge-hotspot, server hotspot1, URL https://captive-portal.firozmahmud.dev/mikrotik/presence. You only must set secret if using the placeholder file.
  4. User Profile hooks On the HotSpot user profile used by guests (check /ip hotspot user print detail): set On Login and On Logout to /system script run portal-presence.
  5. Test Click Run Script once. Check Log for portal-presence: OK, Files → portal-presence-last.json, and /list Connection status.
RouterOS
/ip hotspot user profile set [find name=default] \
  on-login="/system script run portal-presence" \
  on-logout="/system script run portal-presence"

If guests use another profile (not default), set On Login / On Logout on that profile instead. Full click-by-click guide with screenshot: repo docs/mikrotik/presence-script-setup.md.

Walled garden

The portal domain must already be in the walled garden (required for login). That same HTTPS reachability lets the router /tool fetch POST to https://captive-portal.firozmahmud.dev/mikrotik/presence. The Bearer secret authenticates the API — walled garden is not a substitute for the secret.

07 Troubleshooting

Expand the symptom that matches what you see:

Login page never appears
  • Walled garden missing — add captive-portal.firozmahmud.dev to the walled garden
  • Invalid HTTPS certificate — fix TLS on the Nginx side; guests will not bypass a certificate error
  • Login By not HTTP PAP on the active profile from /ip hotspot print detail
Login page loops after every submit

http-pap missing on the active server profile (e.g. only fixed default while hotspot1 uses hsprof4). Run /ip hotspot print detail, then:

RouterOS
/ip hotspot profile set hsprof4 login-by=http-pap,http-chap,cookie
/ip hotspot user profile set [find name=default] idle-timeout=5m keepalive-timeout=2m mac-cookie-timeout=3d
Could Not Identify This Device / no mac=

Guest typed the portal HTTPS URL or dismissed the captive popup. On guest WiFi open /connect or http://neverssl.com. Do not add the kick host to the walled garden. Ensure login.html includes $(mac).

Redirect never fires after joining WiFi

Try an HTTP URL (not HTTPS-only). Confirm Login By includes HTTP PAP on the active server profile.

Form submits but no internet
  • Wrong HotSpot password — match MIKROTIK_HOTSPOT_PASSWORD with /ip hotspot user on the router
  • Missing link-login-only — fix the splash / login.html redirect
  • Unlock host blocked — add the router's login host to MIKROTIK_UNLOCK_HOSTS
  • “Unlock not configured” — set MIKROTIK_HOTSPOT_* in portal .env / GitHub secrets and redeploy
Works in a browser, not on guest WiFi

Router is not redirecting. Recheck the HotSpot profile, interface binding, and hotspot/login.html path.

/list Connection status stuck Offline / presence 503
  • 503 — set MIKROTIK_PRESENCE_SECRET in .env + GitHub; ensure docker-compose.yml passes it; redeploy
  • 401 — script secret ≠ portal secret
  • No status change — On Login / On Logout not set on the correct user profile; or router_ssid / bridge MAC mismatch
  • New submits start Offline until the next presence snapshot includes that MAC — that is expected

08 Security recommendations

POC / developmentBefore production
Keep the HotSpot password only in .env, never in source control.Restrict WinBox/WebFig/SSH to a management subnet or VPN; disable unused services (telnet, ftp, www, api) you do not manage with.
Leave MIKROTIK_UNLOCK_HOSTS unset — the default already trusts only private/loopback IPs.If the router's login URL is not a private IP (e.g. behind a load balancer), set MIKROTIK_UNLOCK_HOSTS explicitly rather than disabling the check.
Change the router's admin password from any default before exposing HotSpot to real guests.Use a strong unique HotSpot password and router admin password; keep RouterOS updated and back up configuration regularly.
Guest history is kept across portal deploys by default (DEPLOY_RESET_DB=false).Only set GitHub variable DEPLOY_RESET_DB=true when you intentionally wipe the guest database (e.g. breaking schema change).

To undo this integration: remove the walled-garden rule and HotSpot user, and restore the default login.html. Keep a RouterOS export from before you started in case you need a full restore.

09 Final checklist

Tick each item before you call the setup done. Progress is saved in this browser.

0 of 0 checklist items complete