Cookbook: MikroTik RouterOS
This chapter is part of the Scenario Cookbook and follows its five-part shape and reading conventions.
MikroTik RouterOS (vendor code 14988) is the most common integration target. ToughRADIUS registers a dedicated vendor enhancer for it; on a successful auth it emits:
Mikrotik-Rate-Limit = "{up}k/{down}k"— a string rate limit (produced bymikrotik_enhancer.go;rx/txis from the router’s perspective, so the first field is the subscriber’s upload).- Plus the standard attributes common to all devices:
Session-Timeout,Acct-Interim-Interval,Framed-Pool,Framed-IP-Address, etc. (produced bydefault_enhancer.go).
Prerequisite: register this router under NAS devices with vendor = MikroTik, the correct source IP and shared secret; ToughRADIUS must be reachable by the device (auth 1812, accounting 1813). If you register it as
Standard, auth still succeeds butMikrotik-Rate-Limitis not emitted.
Scenario A: PPPoE broadband ISP — speed tiers + address pool + expiry disconnect + concurrency
Need / scenario
A neighbourhood / small ISP serves Internet over PPPoE and needs: several speed tiers (e.g. Home = 30M down, Business = 100M down), accounts that disconnect on expiry and cannot redial, a concurrency cap per account (to stop credential sharing), and IPs assigned from a shared address pool.
On the ToughRADIUS side
- Create one rate profile per tier (Rate profiles → New):
- Up / down rate: the unit is Kbps. 30M down means
30720, not30; 10M up means10240. - Concurrency
active_num: e.g.1= at most one online session per account (0= unlimited). - Address pool: a pool name (e.g.
pppoe-pool) that must match the/ip poolname on the router.
- Up / down rate: the unit is Kbps. 30M down means
- Create a user (Users → New): username / password, the matching rate profile (rate, concurrency, pool are inherited from it), an expiry time, status = enabled. For a fixed IP, set a static IPv4 on the user (it overrides the pool).
- Accounting interval: the config item
radius.AcctInterimInterval(default120seconds) sets the emittedAcct-Interim-Interval.
After a successful auth, the Access-Accept actually carries (anchored to
code):
| Attribute | Value | Source |
|---|---|---|
Mikrotik-Rate-Limit | "{up_kbps}k/{down_kbps}k", e.g. 10240k/30720k | mikrotik_enhancer.go |
Session-Timeout | seconds remaining until expiry (drops the current session at the deadline) | default_enhancer.go |
Acct-Interim-Interval | radius.AcctInterimInterval (default 120) | default_enhancer.go |
Framed-Pool | the pool name from the profile / user (emitted only if set) | default_enhancer.go |
Framed-IP-Address | the user’s static IPv4 (emitted only if set) | default_enhancer.go |
The concurrency cap is not implemented by emitting an attribute: it is enforced by
online_count_checkerat auth time, comparingactive_numto the current online count — an over-limit new session is rejected (Access-Reject).
On the device side (RouterOS, reference example, verify on your firmware)
# Point at ToughRADIUS (same shared secret for auth/accounting)
/radius add service=ppp address=<TOUGHRADIUS_IP> secret=<SECRET> timeout=3s
/radius incoming set accept=yes port=3799
# Enable RADIUS auth + accounting + periodic interim updates
/ppp aaa set use-radius=yes accounting=yes interim-update=5m
# The pool name must match the emitted Framed-Pool
/ip pool add name=pppoe-pool ranges=10.10.0.2-10.10.255.254
# PPPoE service (let the RADIUS Framed-Pool/Framed-IP decide remote-address)
/ppp profile add name=radius-pppoe local-address=10.10.0.1
/interface pppoe-server server add service-name=isp interface=<bridge> \
default-profile=radius-pppoe disabled=no
No manual queue is needed for rate limiting: on receiving
Mikrotik-Rate-Limitthe router creates a dynamic simple queue automatically.
Verification
- radtest (server side):
On success it printsgo run ./cmd/radtest auth -server <TOUGHRADIUS_IP> -nas-ip <NAS_IP> \ -username <username> -password <password> -secret <SECRET>Access-Accept; you should seeMikrotik-Rate-Limit,Session-Timeout, and (if a pool is set)Framed-Pool. - Router side:
/ppp active print(online sessions),/queue simple print(the dynamic queue and its rate),/log print where topics~"radius". - Admin UI: the Online sessions page should list the session (Framed IP, duration, up/down traffic).
Troubleshooting (symptom → locate → fix)
- Rate limit has no effect at all → ① confirm the NAS is registered as
MikroTik (registered as Standard emits no VSA); ② wrong rate unit (
30instead of30720); ③ direction reversed (rx/txis the router’s view, the first field is the subscriber’s upload). - Dials up but gets no / wrong IP → the
Framed-Poolname does not match the/ip poolname, or no pool is set on the profile; align the names or set a static IP on the user. - Account still online after expiry →
Session-Timeoutonly drops the current session at the deadline; an already-online session waits for the timeout or a manual forced disconnect on the sessions page. A redial after expiry is rejected byexpire_checker(counted underuser expire). - The second connection is rejected → with
active_num=1,online_count_checkerrejects the concurrent session — this is expected; raise the profile’s concurrency to allow multiple dials. - Server slows down / stops replying after repeated auth failures →
reject_delay_guardintroduces a delay once a username is rejected more than a threshold (default 7) in a row, to throttle brute force; it recovers automatically.
Scenario B: Hotspot + MAC authentication
Need / scenario
In a public WiFi / Hotspot environment, you want certain enrolled devices (printers, IoT, long-term guest devices) to skip the portal and be admitted and rate-limited by MAC address.
On the ToughRADIUS side
ToughRADIUS decides a request is MAC authentication by this rule (anchored to
auth_stages.go / eap_helper.go):
When the MAC address parsed from the request equals the username, the request is treated as MAC auth; the password check then compares against the user record’s MAC address field rather than an ordinary password.
So configure it as follows:
- Create a user with username = the device MAC (the string must match exactly what the router sends), and put the same MAC in the user record’s MAC address field.
- Assign the user a rate profile (rate and concurrency apply as usual).
The biggest trap is MAC format: case and separators must exactly match the
User-Namethe RouterOS hotspot sends (ToughRADIUS compares strings exactly). RouterOS’s format is influenced by settings such asmac-auth-mode— what you store is what must be sent.
On the device side (RouterOS, reference example, verify on your firmware)
/radius add service=hotspot address=<TOUGHRADIUS_IP> secret=<SECRET> timeout=3s
# Enable RADIUS and MAC login on the hotspot server profile
/ip hotspot profile set <profile> use-radius=yes login-by=mac,http-chap
# mac-auth-mode / mac-auth-password depend on firmware
Verification
- radtest: use
-user <MAC> -pwd <MAC>to simulate a MAC auth and check forAccess-Accept. - Router side:
/ip hotspot active printshould list the device;/log printfor the auth log. - Admin UI: the session appears on the Online sessions page.
Troubleshooting (symptom → locate → fix)
- MAC auth always fails → the username / MAC field does not match the MAC
string the router actually sends (case, separators). Capture one request, read
the literal
User-Name, and recreate the user from that exact string. - It is being treated as an ordinary account → only “request MAC == username” takes the MAC-auth path; if the hotspot does not log in by MAC, the request goes through the portal username / password logic.
Scenario C: Live control — CoA, forced disconnect and FUP
Need / scenario
Control online users in real time: rate-limit after a quota is exceeded (FUP), force a re-authentication, or simply kick a session offline.
On the ToughRADIUS side
Select a session on the Online sessions page and run one of two actions
(anchored to session_actions.go and
Admin UI Manual · Online sessions):
- Change of Authorization (CoA-Request): currently carries only
Session-Timeout(#27) and / orFilter-Id(#11).- Use
Session-Timeoutto shorten the session’s life and force the client to re-authenticate sooner. - Use
Filter-Idto have RouterOS apply a pre-defined filter / address-list (e.g. a rate or site-restriction rule).
- Use
- Forced disconnect (Disconnect-Request): terminate the session directly (with a confirmation step).
The correct path to live FUP “speed change”: ToughRADIUS’s CoA does not rewrite
Mikrotik-Rate-Limit. To change a user’s speed live, the standard approach is — change the rate on the profile / user first, then force a disconnect; the client redials automatically and is re-authorized at the new speed. This path matches the system’s actual capability and works for any vendor.
Operator-initiated CoA / Disconnect uses a short timeout with one automatic retry, targeting the CoA port (default 3799) on the NAS record.
On the device side (RouterOS, reference example, verify on your firmware)
# Required, otherwise CoA/Disconnect is not received
/radius incoming set accept=yes port=3799
- The firewall must allow inbound UDP 3799 (from ToughRADIUS to the router).
- For the
Filter-Idapproach, pre-define a filter / queue / address-list of the same name on RouterOS (verify on your firmware).
Verification
- Click Change authorization / Forced disconnect on the sessions page; the result is reported as a notification.
- On the router,
/log printshows the incoming request; after a forced disconnect, the session disappears from/ppp active printand the client then redials automatically.
Troubleshooting (symptom → locate → fix)
- CoA / Disconnect does not respond or times out → ① RouterOS did not set
radius incoming accept=yes; ② the firewall blocks UDP 3799; ③ the NAS record’s CoA port differs from the device’sincoming port; ④ the shared secret differs. - Changed the rate but the online user’s speed did not change → expected: CoA does not change the rate; force a disconnect so the client redials.
- Port confusion → the CoA
1700you often see online is a client-side local port; this system and RFC 5176 use 3799.
Scenario D: WPA2/WPA3-Enterprise Wi-Fi — 802.1X EAP passthrough to ToughRADIUS
Need / scenario
You run enterprise Wi-Fi (WPA2-EAP / WPA3-EAP, i.e. 802.1X) on MikroTik APs
and want one central place for accounts, certificates and policy: ToughRADIUS.
Staff authenticate either with a client certificate (EAP-TLS, password-less),
or with username + password (PEAP-MSCHAPv2 for Windows/AD-style clients, or
EAP-TTLS for legacy / LDAP back ends).
How this differs from
multiduplikator/mikrotik_EAP. That well-known guide makes RouterOS itself the EAP server (ROS6 against the certificate store, or ROS7 via User Manager v5). Here MikroTik is only the authenticator: itseap-methods=passthroughrelays the 802.1X/EAP conversation to ToughRADIUS, and ToughRADIUS terminates EAP. Two consequences follow that you must plan for:
- The trust anchor moves to ToughRADIUS. Clients no longer trust
RouterCA; they must trust the CA that signed ToughRADIUS’s server certificate selected inEapTlsServerCert. You distribute that CA to client devices.- The router holds no user/cert material for auth. All identities live on ToughRADIUS, so you get its account lifecycle, rate profiles, online-session view and accounting for free.
On the ToughRADIUS side
0. Prepare the certificates to import
Generate PEM material first, then import it into ToughRADIUS’s managed
certificate store. A minimal in-house CA with openssl — EC keys keep the TLS
records small, which matters for EAP fragmentation:
# Root CA (distribute ca.pem to every client device)
openssl ecparam -name prime256v1 -genkey -noout -out ca.key
openssl req -x509 -new -key ca.key -sha256 -days 3650 -out ca.pem \
-subj "/CN=ToughRADIUS EAP Root CA"
# RADIUS server certificate (CN/SAN is what clients pin as the server identity)
openssl ecparam -name prime256v1 -genkey -noout -out server.key
openssl req -new -key server.key -out server.csr -subj "/CN=radius.example.com"
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
-days 825 -sha256 -out server.pem \
-extfile <(printf "subjectAltName=DNS:radius.example.com\nextendedKeyUsage=serverAuth\nkeyUsage=digitalSignature,keyEncipherment")
# EAP-TLS client certificate (only for EAP-TLS users). The SAN email becomes the
# RADIUS identity — see the identity-binding rule below.
openssl ecparam -name prime256v1 -genkey -noout -out alice.key
openssl req -new -key alice.key -out alice.csr -subj "/CN=alice"
openssl x509 -req -in alice.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
-days 825 -sha256 -out alice.pem \
-extfile <(printf "subjectAltName=email:[email protected]\nextendedKeyUsage=clientAuth\nkeyUsage=digitalSignature")
# Bundle the client key+cert+CA as PKCS#12 for Windows/Android/iOS enrolment
openssl pkcs12 -export -inkey alice.key -in alice.pem -certfile ca.pem \
-out alice.p12 -passout pass:'<long-passphrase>'
EAP-TLS identity binding (anchored to
tlsengine/identity.go, RFC 5216 §5.2). After the client certificate passes chain validation, ToughRADIUS derives the Peer-Id in this order: SANrfc822Name(email) → SANdnsName→ subjectCN, and it must equal the RADIUSUser-Name(case-insensitive). When a SAN is present theCNis not accepted as an alternate. So with the cert above the matching ToughRADIUS username is[email protected], notalice.
1. Import the material and select managed certificates
Open Certificates and import:
server.pemwith its matchingserver.keyas a server certificate, for example local nameeap-server.ca.pemas the client-CA certificate, for example local nameeap-client-ca.
Then open System config → RADIUS and select those names on the EAP settings
(anchored to internal/app/config_schemas.json /
eap/handlers/tls_config.go):
Config item (radius.*) | EAP-TLS | PEAP / TTLS | Notes |
|---|---|---|---|
EAP Method (EapMethod) | eap-tls | eap-peap / eap-ttls | The method offered first on EAP-Identity. |
Enabled EAP Handlers (EapEnabledHandlers) | eap-tls,eap-peap,eap-ttls | same | Allow-list; * = all. A client may NAK to another method, honoured only if it is enabled here. |
EAP-TLS Server Certificate (EapTlsServerCert) | eap-server | same | Managed certificate presented for all three tunnelled methods; it must include the matching private key. |
EAP-TLS Client CA (EapTlsClientCa) | eap-client-ca | (unused) | Required for EAP-TLS (verifies client certs); PEAP/TTLS are server-only and ignore it. |
EAP-TLS Minimum TLS Version (EapTlsMinVersion) | 1.2 | 1.2 | PEAP/TTLS are pinned to TLS 1.2 regardless. |
Managed certificate selections are resolved between handshakes, so you can rotate imported certificates without restarting the RADIUS service. Until the required managed certificates are selected, EAP safe-rejects (
ErrTLSNotConfigured) rather than authenticating without trust anchors — so a half-configured server never lets anyone in.
2. Create the users
| Method | Username to create | Password field | Comes from |
|---|---|---|---|
| EAP-TLS | the cert Peer-Id, e.g. [email protected] | unused (any placeholder) | cert identity matched in tlsengine/identity.go |
| PEAP-MSCHAPv2 | the login name, e.g. bob | the user’s real password | inner MSCHAPv2 vs GetPassword in peap_inner.go |
| EAP-TTLS (PAP or MS-CHAP-V2) | the login name, e.g. carol | the user’s real password | inner PAP/MSCHAPv2 vs GetPassword in ttls_inner.go |
Each user still needs status = enabled, a future expiry, and a rate profile (rate / pool / concurrency apply exactly as in Scenario A).
The single biggest passthrough trap — the outer (anonymous) identity. ToughRADIUS loads the user record from the outer
User-Nameand looks up the password from that record; mapping a separate anonymous outer identity to the real account is not yet implemented (deferred to M8.4). So for PEAP / TTLS the outer identity must equal the real username — on the supplicant, leave “anonymous identity” blank (it then sends the real username in the clear outer identity) or set it equal to the username. An outer identity ofanonymousis rejected as user not found. EAP-TLS is unaffected (its identity comes from the certificate).
On the device side (RouterOS, reference example, verify on your firmware)
First register this router under NAS devices in ToughRADIUS (its source IP +
shared secret; vendor MikroTik if you also want Mikrotik-Rate-Limit, otherwise
Standard — EAP itself needs no VSA). Then point the AP at ToughRADIUS and make
the security profile pass EAP through:
# 1) RADIUS server for the wireless service (same secret as the NAS record)
/radius add service=wireless address=<TOUGHRADIUS_IP> secret=<SECRET> timeout=3s
# 2a) CLASSIC /interface wireless — passthrough is the key word
/interface wireless security-profiles add name=eap-passthrough \
authentication-types=wpa2-eap eap-methods=passthrough \
radius-eap-accounting=yes \
unicast-ciphers=aes-ccm group-ciphers=aes-ccm
/interface wireless set wlan1 security-profile=eap-passthrough \
ssid="ToughRADIUS-EAP" mode=ap-bridge disabled=no
# 2b) …or CAPsMAN (matches the reference repo's topology)
/caps-man security add name=eap-passthrough authentication-types=wpa2-eap \
encryption=aes-ccm group-encryption=aes-ccm eap-methods=passthrough \
eap-radius-accounting=yes
# attach security=eap-passthrough to your /caps-man configuration, then provision
# 2c) …or the new /interface wifi (wifiwave2, ROS 7.13+): passthrough is IMPLICIT
/interface wifi security add name=eap-sec authentication-types=wpa2-eap,wpa3-eap
/interface wifi set wifi1 security=eap-sec \
configuration.ssid="ToughRADIUS-EAP" disabled=no
eap-methods=passthrough(classic / CAPsMAN) is exactly what makes the router a relay instead of an EAP terminator. The new/interface wifistack has noeap-methodsknob — selecting awpa2-eap/wpa3-eapsecurity profile makes it relay to the configuredservice=wirelessRADIUS automatically.
Verification
radtest cannot drive EAP. Use eapol_test (from wpa_supplicant /
hostap) — the same tool the project’s
EAP acceptance reports run (v2.10). It talks
RADIUS straight to ToughRADIUS, so you can validate the server before
touching a real radio. Save one of these and run
eapol_test -c <file>.conf -a <TOUGHRADIUS_IP> -p 1812 -s <SECRET> — a pass
prints SUCCESS:
# eap-tls.conf — certificate, password-less
network={
key_mgmt=WPA-EAP
eap=TLS
identity="[email protected]" # == the cert SAN email == the TR username
ca_cert="/etc/eap/ca.pem" # trust ToughRADIUS's server cert
client_cert="/etc/eap/alice.pem"
private_key="/etc/eap/alice.key"
}
# peap-mschapv2.conf
network={
key_mgmt=WPA-EAP
eap=PEAP
identity="bob" # outer == real username (no anonymous id)
password="<bob-password>"
ca_cert="/etc/eap/ca.pem"
phase2="auth=MSCHAPV2"
}
# ttls-pap.conf
network={
key_mgmt=WPA-EAP
eap=TTLS
identity="carol"
anonymous_identity="carol" # must equal identity (see the trap above)
password="<carol-password>"
ca_cert="/etc/eap/ca.pem"
phase2="auth=PAP" # or auth=MSCHAPV2; CHAP/MS-CHAP unsupported
}
- ToughRADIUS log: a successful run logs
radius auth success … is_eap=true result=success; the session appears on the Online sessions page (with accounting once the radio is live). - Router side:
/log print where topics~"radius,wireless"; for an associated client/interface wireless registration-table print(classic) or/interface wifi registration-table print(wifi). - EAP-TLS and EAP-TTLS (PAP & MS-CHAP-V2) verify cleanly end-to-end with
eapol_test. For PEAP-MSCHAPv2,eapol_testcurrently hits a documented inner-framing interop gap, so the acceptance harness validates PEAP with the in-process integration test instead (see EAP Acceptance Reports); real Windows / Android / iOS PEAP supplicants interoperate, so test PEAP with an actual client.
Troubleshooting (symptom → locate → fix)
- EAP-TLS reject, reply
… handshake failed→ the client certificate does not chain to the managedEapTlsClientCaCA (or the wrong CA was selected). Re-issue the client cert from the sameca.pem, or select the correct imported client CA. - EAP-TLS reject, reply
… identity … does not match→ the user’s username ≠ the cert Peer-Id. Remember the order SAN email → SAN DNS → CN and that a SAN cert’sCNis ignored: for the sample cert the username must be[email protected]. Either rename the user or issue a CN-only cert and set the username to the CN. - Client refuses to connect / “can’t verify server” → the device does not
trust ToughRADIUS’s CA. Install
ca.pemon the client; on Android 11+ also set the Domain field to the server cert CN/SAN (radius.example.com), which Android now enforces. - PEAP / TTLS reject as “user not found” although the password is right → an
anonymous outer identity was used. Clear it (or set it to the real username)
so the outer
User-Nameequals the account — see the trap above. - No EAP challenge at all / immediate reject →
EapTlsServerCert(andEapTlsClientCafor EAP-TLS) is not selected, or the selected server certificate has no private key, so EAP safe-rejects. Import the certificate material and select the managed names; no restart needed. - The method you want is never offered → the default
EapMethodiseap-md5, which is invalid for WPA-Enterprise. SetEapMethodto your tunnelled method and include it (and any you accept via client NAK) in Enabled EAP Handlers. - EAP-TTLS inner auth rejected → only inner PAP and MS-CHAP-V2 are implemented; inner CHAP / MS-CHAP / tunnelled-EAP are not. Also the TTLS tunnel is pinned to TLS 1.2 — a TLS-1.3-only supplicant won’t complete phase 2.
- Auth works but no accounting / no online session → enable
radius-eap-accounting=yes(classic) /eap-radius-accounting=yes(CAPsMAN) and make sure UDP 1813 reaches ToughRADIUS.
Related chapters
- Vendor Integration Guide · MikroTik — attribute reference card.
- Admin UI Manual — users / rate profiles / online sessions / CoA forms.
- FAQ — more cross-scenario troubleshooting Q&A.