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.
- Guest login
- https://captive-portal.firozmahmud.dev/login
- Captive kick
- https://captive-portal.firozmahmud.dev/connect
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.
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:
- Join WiFi Guest connects and receives an IP from the router.
-
Get redirected
Any website visit sends the guest to
/loginwithmac=andlink-login-only. - Submit form Guest enters name, age, gender (Male / Female / Other) and submits — submit = consent (no checkbox).
-
Portal unlocks
Portal saves the record and POSTs HotSpot credentials to
$(link-login-only). -
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.
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.
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.
/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><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.
/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:
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.
/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.
| Portal | MikroTik |
|---|---|
portal-guest (env MIKROTIK_HOTSPOT_USER) | /ip hotspot user name |
MIKROTIK_HOTSPOT_PASSWORD | same password on the router |
/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 injectmac= - If you dismissed the captive popup: open
/connect(portal kick → router intercept →/login?mac=…) - Confirm the address bar has
mac=,link-login-only=, and (fromlogin.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.
05 After the guest submits
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.
-
Portal secret
Set the same value in portal
.env/ GitHub secretMIKROTIK_PRESENCE_SECRETand inside the MikroTik script. Compose must pass it into the portal container; redeploy. Empty secret → API returns 503. -
System → Scripts → New
Name exactly
portal-presence(notscript1). Check Don’t Require Permissions. Paste source from reposetup/mikrotik/presence-report.rsc(or gitignoredpresence-report.local.rscwith secret already filled). -
Edit CONFIG
POC defaults already match: SSID
captive-portal-open-wifi, bridgebridge-hotspot, serverhotspot1, URLhttps://captive-portal.firozmahmud.dev/mikrotik/presence. You only must setsecretif using the placeholder file. -
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. -
Test
Click Run Script once. Check Log for
portal-presence: OK, Files →portal-presence-last.json, and/listConnection status.
/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:
08 Security recommendations
| POC / development | Before 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