Skip to content
deangoldhill edited this page Apr 6, 2026 · 27 revisions

RadiusStack Documentation

Welcome to the RadiusStack Wiki! This guide covers everything from deployment to daily administration.

Table of Contents

  1. Overview
  2. Architecture
  3. Installation & Deployment
  4. High Availability
  5. Features & Guides

Overview

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.

Architecture

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
Loading

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)
Loading

Installation & Deployment

Prerequisites

  • Docker and Docker Compose installed.
  • Ports 1812/udp (Auth), 1813/udp (Acct), and 80/443 (Web UI/API) available on your host.

Quick Start

  1. Clone the repository to your host machine:
    git clone https://github.com/deangoldhill/RadiusStack.git
    cd RadiusStack
  2. Edit container_config.env to set your desired passwords and token key.
  3. Bring the stack online:
    docker compose --env-file container_config.env up -d --build
  4. Access the Web UI at http://<your-server-ip>.
  5. Login with the default credentials (admin / admin).

High Availability

High Availability (HA) Architecture

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.


How the Sync Engine Works

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:

  1. 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.
  2. RADIUS Writes (The Poller): Because the FreeRADIUS daemon writes to the database directly (bypassing the Node API), a background worker continuously monitors the radacct and radpostauth tables for new IDs and timestamps. When it detects new network activity, it converts the rows into SQL statements and pushes them into the ha_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).


HA Execution Pipeline

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
Loading

Configuration & Environment Variables

To enable High Availability, you must configure the HA variables in your container_config.env file on both servers.

Primary Node Configuration

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

Secondary Node Configuration

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_TOKEN is a long, cryptographically secure string (e.g., generated via openssl rand -hex 32). Do not expose port 3000 on the Secondary Node to the public internet without a reverse proxy (like NGINX) enforcing HTTPS.


Failover & Promotion

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"

Features & Guides

Managing Users & TOTP 2FA

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.

MAC Authentication

MAC Authentication (MAB)

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.

How to Configure MAC Authentication

In RadiusStack, a MAC address is simply treated as a standard user.

  1. 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:ff or AABBCCDDEEFF
    • Ruckus / Aruba might use: aa-bb-cc-dd-ee-ff
  2. 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).
  3. Assign a Profile:
    • Assign the device to an appropriate profile (e.g., an IoT_Devices group).
    • 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).

Best Practices for MAC Authenitcation

  • Dedicated Profiles: Always put MAC-authenticated devices into their own restricted Profile. MAC addresses are easily spoofed, so you should ensure that the IoT_Devices profile 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 & Custom Attributes

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

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) or never.
  • 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.

NAS Clients

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 Secret key which must be mirrored on the client device.
  • FreeRadius service must be restarted when adding/modifying NAS clients, via them settings page.

Administrators & 2FA

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 Administrators to 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-Key that 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.

Certificates (EAP/SSL)

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.

Database Backup & Restore

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.

Reporting and analyitcs

  • 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 and tester

  • ** 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