-
Notifications
You must be signed in to change notification settings - Fork 1
Home
Welcome to the RadiusStack Wiki! This guide covers everything from deployment to daily administration.
RadiusStack is a powerful, self-contained AAA (Authentication, Authorization, and Accounting) platform built on FreeRADIUS 3, MariaDB, and a custom Node.js API backend. It removes the steep learning curve traditionally associated with FreeRADIUS by providing an intuitive Web UI and a fully documented REST API.
It supports EAP (PEAP + TTLS) with PAP, CHAP and MSCHAPV2 out of the box, suitable for WIFI (WPA-Enterprise) authentication, and is pre-configured to copy reply attributes from the inner to outer packet for NAS visibility, e.g dynamic VLAN assignment.
The system relies on Docker containers, ensuring clean isolation and high portability across any host machine.
Instead of using custom container images, the platform is purposfully built using official container images. Initialization and container configuration is performed during deployment. The platfom conisits of 5 containers which are all deployed using a single docker compose script. Containers can be upgraded in-place by re-running the docker compose script.
- Radius Server: The freeradius version 3.x server.
- Database Server: MariaDB server. Used by the radius server and API server.
- PHPMyAdmin: A web frontend for direct managment access to the backend database. Warning - access to this serivice should be restrried
- Node.js APi Server: The backend API server
- WebUI: The WebUI frontend. All the functions in the WebUI are API calls via the API server.
Application:
sequenceDiagram
autonumber
box rgb(245, 245, 245) Administrator Environment
participant Admin as Admin User
participant UI as WebUI (Browser)
end
box LightBlue Node.js Application Stack
participant API as API Server (TCP :3000)
participant Auth as JWT / Role Middleware
participant Audit as Audit Logger
end
box rgb(255, 243, 224) Database Tier
participant DB as MariaDB (TCP :3306)
end
%% ==========================================
%% Phase 1: Authentication & Authorization
%% ==========================================
Note over Admin, DB: Phase 1: Admin Login & Session Creation
Admin->>UI: Enters Username & Password
UI->>API: POST /auth/login (HTTP JSON)
API->>DB: SELECT * FROM admins WHERE username = ?
DB-->>API: Returns Password Hash & 2FA Status
Note over API: bcrypt.compare() validates hash
API->>Audit: Log: "Login attempt - Password validated"
Audit->>DB: INSERT INTO admin_audit_log
API-->>UI: HTTP 200: { status: 'needs_2fa_verify', token: JWT }
Admin->>UI: Enters TOTP Code
UI->>API: POST /auth/verify-2fa (Token + Code)
Note over API: Verifies JWT & checks TOTP code against DB secret
API->>Audit: Log: "2FA verification - TOTP validated"
Audit->>DB: INSERT INTO admin_audit_log
API-->>UI: HTTP 200: Returns persistent API_KEY
%% ==========================================
%% Phase 2: Interacting with the UI (Read)
%% ==========================================
Note over Admin, DB: Phase 2: Loading Dashboard Data
Admin->>UI: Clicks "Users" tab
UI->>API: GET /api/users <br/>Header: X-API-Key
API->>Auth: requireApiAuth('users', 'read-only')
Auth->>DB: SELECT permissions FROM admins WHERE api_key = ?
DB-->>Auth: Returns { users: 'read-write' }
Note over Auth: Permission granted. Request proceeds.
API->>DB: SELECT * FROM radcheck, radusergroup, etc.
DB-->>API: Returns raw RADIUS tables
Note over API: Formats SQL data into clean JSON arrays
API-->>UI: HTTP 200: [User List JSON]
UI-->>Admin: Renders Data Table
%% ==========================================
%% Phase 3: Making System Changes (Write)
%% ==========================================
Note over Admin, DB: Phase 3: Admin Modifies a Record
Admin->>UI: Edits user & clicks "Save"
UI->>API: PUT /api/users/:username (HTTP JSON) <br/>Header: X-API-Key
API->>Auth: requireApiAuth('users', 'read-write')
Auth->>DB: Verify API Key & 'read-write' permission
DB-->>Auth: Permission Verified
API->>DB: BEGIN TRANSACTION
API->>DB: UPDATE radcheck SET value = ? WHERE username = ?
API->>DB: UPDATE radreply SET value = ? WHERE username = ?
API->>DB: COMMIT TRANSACTION
API->>Audit: Log: "Updated user [username]"
Audit->>DB: INSERT INTO admin_audit_log (origin: 'webui', action: 'Update')
API-->>UI: HTTP 200 OK (Success)
UI-->>Admin: Shows "Saved Successfully" Toast
RADIUS:
sequenceDiagram
autonumber
box rgb(245, 245, 245) Network Edge
participant Client as Client Device (Phone/PC)
participant NAS as NAS (Wi-Fi AP/Router/VPN)
end
box LightGreen RADIUS Core (rlm_sql)
participant FR as FreeRADIUS (UDP :1812/1813)
end
box rgb(255, 243, 224) Database Tier
participant DB as MariaDB (TCP :3306)
end
%% ==========================================
%% Phase 1: Authentication & Authorization
%% ==========================================
Note over Client, DB: Phase 1: Access Request (Authentication)
Client->>NAS: Attempts connection (SSID/VPN)
NAS->>FR: Access-Request (UDP :1812)<br/>User-Name, User-Password, NAS-IP-Address
Note over FR: rlm_sql module triggers "authorize" query
FR->>DB: SELECT id, username, attribute, value, op FROM radcheck WHERE username = ?
DB-->>FR: Returns Cleartext-Password, Profile DB attributes
Note over FR: FreeRADIUS verifies core credentials internally
%% NAS Restriction Logic
alt NAS Restriction Enabled (Profile Level)
FR->>DB: SELECT attribute FROM radgroupcheck WHERE groupname = ?
DB-->>FR: Returns allowed NAS-IP-Address values
Note over FR: Validates incoming NAS-IP against DB list.<br/>If ANY one of the NAS-IP-Addresses match = ACCEPT.<br/>If none match = REJECT.
end
%% Usage Plan Enforcement
alt Usage Plan Enforcement (User Level)
FR->>DB: SUM(acctinputoctets + acctoutputoctets) FROM radacct
DB-->>FR: Returns current usage & plan quotas
Note over FR: Compares total usage/time against assigned Plan limits.<br/>If Usage > Plan Limit = REJECT.
end
%% Policy Retrieval & Logging
FR->>DB: SELECT attribute, value, op FROM radreply / radgroupreply
DB-->>FR: Returns Network Policies (e.g., Framed-IP, WISPr-Bandwidth)
FR->>DB: INSERT INTO radpostauth (username, pass, reply, authdate)
DB-->>FR: Logs Authentication Attempt (Success or Failure)
%% Accept/Reject Decision
alt All Checks Pass
FR-->>NAS: Access-Accept (UDP :1812)<br/>Includes injected Network Policies
NAS-->>Client: Grants Network Access
else Credentials, NAS, or Plan limits failed
FR-->>NAS: Access-Reject (UDP :1812)
NAS-->>Client: Denies Network Access
end
%% ==========================================
%% Phase 2: Accounting (Session Start)
%% ==========================================
Note over Client, DB: Phase 2: Accounting Start (Tracking begins)
NAS->>FR: Accounting-Request (UDP :1813)<br/>Acct-Status-Type = Start, Acct-Session-Id
Note over FR: rlm_sql module triggers "accounting start"
FR->>DB: INSERT INTO radacct (radacctid, username, nasipaddress, <br/>acctstarttime, acctsessionid, framedipaddress...)
DB-->>FR: Returns Success (Insert ID)
FR-->>NAS: Accounting-Response (UDP :1813)
%% ==========================================
%% Phase 3: Accounting (Interim Updates)
%% ==========================================
Note over Client, DB: Phase 3: Interim Updates (Live bandwidth tracking)
Client->>NAS: Streams video, downloads files...
loop Every 5-10 minutes (NAS Configured)
NAS->>FR: Accounting-Request (UDP :1813)<br/>Acct-Status-Type = Interim-Update<br/>Acct-Input-Octets, Acct-Output-Octets
FR->>DB: UPDATE radacct SET acctupdatetime = NOW(), <br/>acctsessiontime = ?, acctinputoctets = ?, acctoutputoctets = ? <br/>WHERE acctsessionid = ?
DB-->>FR: Returns Success
FR-->>NAS: Accounting-Response (UDP :1813)
end
%% ==========================================
%% Phase 4: Accounting (Session End)
%% ==========================================
Note over Client, DB: Phase 4: Accounting Stop (Session terminates)
Client->>NAS: Disconnects / Roams away
NAS->>FR: Accounting-Request (UDP :1813)<br/>Acct-Status-Type = Stop<br/>Acct-Terminate-Cause
FR->>DB: UPDATE radacct SET acctstoptime = NOW(), <br/>acctsessiontime = ?, acctinputoctets = ?, acctoutputoctets = ? <br/>WHERE acctsessionid = ?
DB-->>FR: Returns Success
FR-->>NAS: Accounting-Response (UDP :1813)
- Docker and Docker Compose installed.
- Ports
1812/udp(Auth),1813/udp(Acct), and80/443(Web UI/API) available on your host.
- Clone the repository to your host machine:
git clone https://github.com/deangoldhill/RadiusStack.git cd RadiusStack - Edit
container_config.envto set your desired passwords and token key. - Bring the stack online:
docker compose --env-file container_config.env up -d --build
- Access the Web UI at
http://<your-server-ip>. - Login with the default credentials (
admin/admin).
RadiusStack features a built-in, application-layer High Availability (HA) sync engine. Instead of relying on complex third-party database replication (like Galera or MySQL Master-Slave), RadiusStack natively handles its own state synchronization over standard HTTP/REST APIs.
This allows you to deploy a Primary Node (Active) and a Secondary Node (Standby) across different physical locations, subnets, or data centers without exposing direct database ports (3306) across the WAN.
Because RadiusStack receives database writes from two completely different sources (Admin actions in the WebUI and Network Access actions from FreeRADIUS), the HA Sync Engine uses a dual-capture architecture:
-
API/WebUI Writes (The Interceptor): Any change made by an Administrator (e.g., creating a user, assigning a profile, updating settings) is intercepted natively at the Node.js database pool layer. The exact SQL query is captured and pushed directly to a local
ha_queue. -
RADIUS Writes (The Poller): Because the FreeRADIUS daemon writes to the database directly (bypassing the Node API), a background worker continuously monitors the
radacctandradpostauthtables for new IDs and timestamps. When it detects new network activity, it converts the rows into SQL statements and pushes them into theha_queue.
Once queued, the Primary Node packages the queries and transmits them securely to the Secondary Node's API, which executes them in perfect parity (automatically bypassing auto-increment offsets).
The sequence diagram below illustrates how both Admin changes and Network activity are unified into a single, secure HA transit pipeline.
sequenceDiagram
autonumber
box LightBlue Primary Node (Active)
participant UI as WebUI / Admin API
participant FR as FreeRADIUS
participant DB1 as MariaDB (Primary)
participant Sync as HA Sync Engine
end
box rgb(243, 229, 245) Transit Network
participant LAN as HTTP / HTTPS
end
box LightGreen Secondary Node (Standby)
participant API as API Receiver
participant DB2 as MariaDB (Secondary)
end
%% Phase 1A: WebUI / API Writes (Interceptor)
Note over UI, DB1: Scenario A: Admin changes user via WebUI
UI->>DB1: Pool Query (e.g. UPDATE radcheck)
DB1-->>Sync: MySQL Pool Interceptor catches WRITE query
Sync->>DB1: Parses SQL & parks in local ha_queue
%% Phase 1B: FreeRADIUS Writes (Cursor)
Note over FR, DB1: Scenario B: NAS updates session data
FR->>DB1: Writes AAA Record natively
Sync->>DB1: Poller detects new IDs / timestamps
DB1-->>Sync: Returns unmatched rows
Sync->>DB1: Converts to SQL & parks in local ha_queue
%% Phase 2: Transit (Unified)
Note over Sync, LAN: Unified HA Pipeline
Sync->>Sync: processHaQueue() reads batch of rows
Sync->>LAN: POST /api/sync/execute (JSON Payload)
%% Phase 3: Validation & Execution
LAN->>API: Delivers Payload (Authorized via HA_API_TOKEN)
API->>DB2: SET auto_increment_increment = 1 (Bypass Offset)
API->>DB2: Executes identical SQL statement
DB2-->>API: Returns Success (Row Inserted)
%% Phase 4: Feedback
API-->>LAN: HTTP 200 OK
LAN-->>Sync: Forwards 200 OK
Sync->>DB1: Deletes row from ha_queue
To enable High Availability, you must configure the HA variables in your container_config.env file on both servers.
The Primary Node accepts all incoming writes and is responsible for pushing data to the peer.
HA_ENABLED=true
HA_ROLE=primary
HA_DB_OFFSET=1 # Set to 2 on the secondary node
HA_PEER_IP=192.168.1.51
HA_API_TOKEN=changeme_api_sync_token
The Secondary Node operates in read-only mode for the WebUI. It rejects standard API writes and only accepts replicated SQL payloads from the Primary Node that include the matching HA_API_TOKEN.
HA_ENABLED=true
HA_ROLE=secondary
HA_DB_OFFSET=2 # Set to 2 on the secondary node
HA_PEER_IP=192.168.4.50
HA_API_TOKEN=changeme_api_sync_token
Security Note: Ensure your
HA_API_TOKENis a long, cryptographically secure string (e.g., generated viaopenssl rand -hex 32). Do not expose port 3000 on the Secondary Node to the public internet without a reverse proxy (like NGINX) enforcing HTTPS.
If the Primary Node goes offline, the Secondary Node contains a perfect, up-to-date replica of your RADIUS environment.
By default, the Secondary Node's WebUI is locked in read-only mode to prevent split-brain database scenarios. To promote the Secondary Node to become the new Primary (allowing you to make Admin changes), you can send an API request to the promotion endpoint:
In the settings page, click 'promote to primary'
Or via API:
curl -X POST http://<SECONDARY_IP>:3000/api/ha/promote \
-H "Authorization: Bearer your-admin-api-key"The Users module allows you to create users and assign profiles and usage plans.
TOTP (Multi-Factor Authentication):
- RadiusStack supports adding TOTP (e.g., Google Authenticator) to RADIUS users on a per user basis (TOTP only supports PAP authentication).
- To Enable: Toggle the TOTP switch next to a user. The UI will instantly generate a one-time registration code and a secure registration URL.
- Registration: Provide the user with the URL and code. They will log in via the web portal using their standard RADIUS password + the one-time code to receive their scannable QR Code. *By default the registration code is valid for 24 hours, but can be modified in the settings page.
-
Authentication: Once registered, the user authenticates by appending their 6-digit TOTP code directly to the end of their normal password (e.g.,
password123456). *Users without TOTP enabled will be able to login with just their username and password. Therefore, it is highly recomended users without TOTP (e.g. for WIFI authenticaiton) be assigned to a profile restricted by NAS IP (Access Points) otherwise a brute force attack may still be possible for these users. *Note - The freeRadius TOTP module only works with PAP because the radius server needs to see the code in order to validate it against the TOTP key on record. It will not work with a challange password protocol. It will still work with EAP but only when using PAP as the password protocol.
RadiusStack natively supports MAC Authentication Bypass (MAB). This is incredibly useful for authenticating "headless" or IoT devices that cannot type in a username or password (e.g., Smart TVs, Printers, Game Consoles, or corporate VoIP phones).
Instead of prompting for credentials, your Network Access Server (NAS) reads the device's MAC address and automatically queries the RADIUS server in the background.
In RadiusStack, a MAC address is simply treated as a standard user.
-
Check your NAS format: Different vendors send MAC addresses in different formats. Check your router/switch documentation or check the RadiusStack connection logs to see how it is formatting the request.
- Cisco / Ubiquiti often use:
aa:bb:cc:dd:ee:fforAABBCCDDEEFF - Ruckus / Aruba might use:
aa-bb-cc-dd-ee-ff
- Cisco / Ubiquiti often use:
-
Create the "MAC" User:
- Navigate to the Users tab in the WebUI.
- Click Add User.
-
Username: Enter the device's MAC address exactly as your NAS sends it (e.g.,
aa:bb:cc:dd:ee:ff). - Password: Enter the MAC address again. (Note: Most networking vendors send the MAC address as both the username and the password by default during MAB. If your vendor uses a specific static password for MAB, enter that instead).
-
Assign a Profile:
- Assign the device to an appropriate profile (e.g., an
IoT_Devicesgroup). - You can use the Profile's custom reply attributes to dynamically assign the device to a specific VLAN (e.g.,
Tunnel-Type = 13,Tunnel-Private-Group-Id = 40).
- Assign the device to an appropriate profile (e.g., an
-
Dedicated Profiles: Always put MAC-authenticated devices into their own restricted Profile. MAC addresses are easily spoofed, so you should ensure that the
IoT_Devicesprofile has restricted firewall/VLAN access and is not granted the same privileges as your secure WPA2-Enterprise users. - NAS IP Restrictions: If you only have one or two specific switches that handle IoT devices, use the NAS IP Restriction feature on the Profile to ensure MAC authentication is only accepted from those specific network switches.
- Accounting: RadiusStack tracks bandwidth for MAC users exactly the same way it tracks human users. You can apply data or session limits directly to the MAC address if you want to cap the bandwidth of a specific device.
Profiles act as permission groups for your users. Instead of configuring attributes for every single user, you create a Profile and assign users to it.
- VLAN assignment: specify a VLAN ID. All supporting attributes for dynamic VLAN assignment will automatically be added.
-
Reply Attributes: You can inject any standard FreeRADIUS dictionary attribute (e.g.,
Filter-Id,acct-interim-interval, Vendor-Specific). Select form a list of commonly used attributes or add additional attributes to the list via the settings page. - NAS Restrictions: You can optionally lock a profile so its users can only authenticate from a list of specific NAS IP addresses. *In a standard radius deployment, only 1 NAS-IP-Address check attribute can be specified. But RadiusStack uses a custom ULANG policy that allows multiple IP addresses to be listed in the profile, a match of any one will be accepted.
Usage plans enforce strict data and or time limits on user sessions.
- Data/Time Caps: Define limits such as "10 GB data" or "24 hours connection time".
-
Reset Cycles: Set plans to reset automatically (
daily,monthly) ornever. - If a user breaches their limit, RadiusStack will dynamically reject their Access-Requests until the cycle resets.
- Manually reset usage cycle in the users page.
The NAS (Network Access Server) module defines which network devices are permitted to query RadiusStack.
- Add switches, firewalls, or Wi-Fi access points by IP address.
- Set a strong, shared
Secretkey which must be mirrored on the client device. - FreeRadius service must be restarted when adding/modifying NAS clients, via them settings page.
The Admins tab manages who has access to the RadiusStack Web UI and REST API.
- Granular Permissions: Restrict admins to specific modules (e.g., allow an admin to manage Users, but not profiles and Settings) Grant read-only or read-write permission.
-
2FA Enforcement: You can toggle 2FA on a per-admin basis. Alternatively, navigate to Settings and check
Enforce 2FA for all Administratorsto mandate security globally. When an admin with 2FA enabled logs in for the first time, they will be forced to register 2FA. -
API Keys: Every admin has a unique
X-API-Keythat can be regenerated at any time for programmatic access. *Note - when an API key is regenerated, the webui session will be terminated because the webui uses the API key to communicate with the backend API.
For advanced 802.1X enterprise network deployments (like WPA2-Enterprise), FreeRADIUS requires TLS certificates. *Upon deployment, a 10 year certificate will be generated.
- Auto-Generate: Navigate to the Settings tab and fill out the form to have the backend automatically generate a self-signed root CA and Server certificate.
- CA cert download: to avoid the trust prompt on the client device, download and install the root CA certificate
- Custom certificate Upload: You can upload your own pre-signed private key and server certificate.
- Changes to certificates automatically trigger a safe restart of the RADIUS container to apply the new keys.
Located in the Settings page. Backup is exported in JSON format with two options
- Full Backup: Generates a full database backup including all objects, settings and accounting / authenticaton / audit logs. *Note - full backups may fail to restore if imported to a different version as the database schema may be different.
- Object Backup: Generates a partial backup of users, profiles, plans and NAS clients. and optionally include accounting / authenticaton / audit logs. This contains most of the data needed to restore the server.
- The partial backup should safely import to different versions because the structure of these tables should never change.
- Backup Restore: Upload a previously generated backup. RadiusStack intelligently maps the columns, ignoring obsolete schemas to prevent database crashes when upgrading to newer server versions.
- Dashboard: The dashboard show most recent active sessions authenticaiton logs. Click an active session to view full details of the session.
- Auth Log: View Authentication logs with search capability. This log will also show authentication attempts of unknown users.
- **Accounting: View accounting records both active and closed. Filter by username, NAS client and date/time range. Click an accounting record to view full details. *You can also purge accounting records for the specified filter scope.
- Audit Log: The Audit Log tracks every action taken by Administrators (creations, deletions, setting changes). It shows the origin (webUI, API, System), result (success, failed, denied) and action attempted. *Filter by date range, result or search for action keywords.
- Reports: The top of the reports page shows the heaviest users by data usage and session time. *The failed authetnication report lists unique users with failed authentication by count, useful for detecting brute force attacks. *The user-specific report generates a detailed report of activity for a specific user including top sesssions, total requests, total data and session time for the specified date range.
- Module Stats: Click an object in the users, MAC Auth, Profiles and Plans pages to view related stats
- **Container logs: All containers are configured to redirect service logs to the container logs. View the container logs from the settings page.
- ** API schema:** The API Docs page lists the entire API schema
- Interactive API Tester: Click one of the API endpoints to pre-populate the tester with the correct URL and modifiable sample data. Optionally generate a curl command to test the API call from a different host. *The API tester automatically uses the API key for the logged in administrator