Introduction
Every time you connect to a corporate WiFi, log into a VPN, or dial into an ISP, a RADIUS transaction happens behind the scenes. RADIUS (Remote Authentication Dial-In User Service) is the protocol that provides centralized Authentication, Authorization, and Accounting (AAA) for network access — it’s been the industry standard since 1991 and remains the backbone of enterprise network access control in 2026.
This guide covers RADIUS protocol mechanics, attributes, EAP methods, FreeRADIUS configuration, advanced deployments, security considerations, and troubleshooting. Whether you’re setting up 802.1X WiFi authentication, VPN access, or network device administration, understanding RADIUS is essential.
What is RADIUS?
RADIUS is a client-server protocol defined in RFC 2865 (Authentication/Authorization) and RFC 2866 (Accounting). It enables Network Access Servers (NAS) — WiFi access points, VPN gateways, switches — to authenticate users against a centralized server and receive authorization parameters for the session.
Architecture
┌──────────────────┐
User ───► NAS ────┤ RADIUS Server │
│ (FreeRADIUS, NPS) │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Identity Source │
│ (LDAP, AD, SQL) │
└──────────────────┘
The NAS acts as a RADIUS client, forwarding authentication requests from end users to the RADIUS server. The server validates credentials against an identity source (Active Directory, LDAP, local database, token server) and returns authorization attributes — IP address, VLAN assignment, access policy, session timeout, etc.
Key Features
- Centralized AAA: Single policy enforcement point for all network devices
- Attribute-Based: Flexible authorization via attribute-value pairs
- Proxy Support: Chain multiple RADIUS servers across organizational boundaries
- Accounting: Track usage, duration, and data transfer per session
- Vendor Extensibility: Vendor-Specific Attributes (VSA) for device-specific policy
- Stateless Protocol: UDP-based, simple request-response model
RADIUS vs Diameter
| Aspect | RADIUS | Diameter |
|---|---|---|
| Transport | UDP (1812/1813) | TCP/SCTP (3868) |
| Security | Shared secret + MD5 hash | IPSec/TLS |
| Reliability | Application-level retransmit | Transport-level reliable |
| Session management | Basic | Rich (session binding, routing) |
| Complexity | Low | High |
| Primary use | Network access (WiFi, VPN) | Mobile/LTE, IMS |
| Adoption | Ubiquitous enterprise | Telco/core network |
RADIUS remains dominant for enterprise LAN/WiFi/VPN access. Diameter is primarily used in carrier-grade environments (3G/4G/5G core networks).
Protocol Mechanics
Message Types
| Type | Code | Direction | Description |
|---|---|---|---|
| Access-Request | 1 | NAS → Server | Authentication request |
| Access-Accept | 2 | Server → NAS | Authentication approved + authorization |
| Access-Reject | 3 | Server → NAS | Authentication denied |
| Accounting-Request | 4 | NAS → Server | Start/stop/interim accounting |
| Accounting-Response | 5 | Server → NAS | Accounting acknowledgment |
| Access-Challenge | 11 | Server → NAS | Challenge for EAP/CHAP |
| Status-Server | 12 | Both | Server health check |
| Status-Client | 13 | Both | Client status |
| Disconnect-Request | 40 | Server → NAS | Terminate session (RFC 5176) |
| Disconnect-ACK | 41 | NAS → Server | Session terminated |
| Disconnect-NAK | 42 | NAS → Server | Cannot terminate |
| CoA-Request | 43 | Server → NAS | Change authorization (RFC 5176) |
| CoA-ACK | 44 | NAS → Server | Authorization changed |
| CoA-NAK | 45 | NAS → Server | Cannot change |
Authentication Flow
User NAS (Client) RADIUS Server
| | |
| Connect request | |
|------------------->| |
| | Access-Request |
| | (User-Name, |
| | User-Password/CHAP, |
| | NAS-IP-Address, |
| | Service-Type) |
| |---------------------->|
| | |
| | Access-Challenge |
| | (if EAP/CHAP) |
| |<----------------------|
| Challenge | |
|<-------------------| |
| Response | |
|------------------->| |
| | Access-Request v2 |
| |---------------------->|
| | |
| | Access-Accept |
| | (Framed-IP-Address, |
| | Session-Timeout, |
| | Filter-ID, |
| | VLAN assignment) |
| |<----------------------|
| Access Granted | |
|<-------------------| |
| | Accounting-Request |
| | (Start) |
| |---------------------->|
| | Accounting-Response |
| |<----------------------|
Packet Format
Every RADIUS packet has a fixed header followed by variable-length attributes:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─────────────────────────────────────────────────────────────────┤
│ Code (1) │ Identifier (1) │ Length │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Authenticator (16 bytes) │
│ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ Attributes (variable length) ... │
└─────────────────────────────────────────────────────────────────┘
- Code: Message type (1–45)
- Identifier: Matches requests to responses (sequence number)
- Length: Total packet length (20–4096 bytes)
- Authenticator: Request authenticator (MD5 hash of shared secret + packet)
- Attributes: Type-Length-Value triplets
RADIUS Security Model
RADIUS uses a shared secret between client and server (not per-user passwords). The secret is used to:
- Encrypt the
User-Passwordattribute (MD5 XOR, weak — vulnerable to dictionary attacks) - Generate the
Response Authenticatorfor packet integrity - Hide attributes using
Tunnel-Passwordencryption
Warning: The MD5-based encryption for User-Password is cryptographically weak. For production deployments, use EAP methods with TLS (EAP-TLS, PEAP) instead of PAP/CHAP, and tunnel RADIUS over RadSec or IPsec.
RADIUS Attributes
Standard Attributes Reference
Attributes are the heart of RADIUS — every policy decision is expressed as attribute-value pairs.
| ID | Name | Type | Description |
|---|---|---|---|
| 1 | User-Name | String | User login name |
| 2 | User-Password | Encrypted | Password (MD5 XOR) |
| 3 | CHAP-Password | Binary | CHAP response |
| 4 | NAS-IP-Address | IP | Address of the NAS |
| 5 | NAS-Port | Integer | Physical/virtual port on NAS |
| 6 | Service-Type | Enum | Framed, Login, Authenticate-Only, etc. |
| 7 | Framed-Protocol | Enum | PPP, SLIP, ARAP, etc. |
| 8 | Framed-IP-Address | IP | Assign this IP to user |
| 9 | Framed-IP-Netmask | IP | Subnet mask |
| 10 | Framed-Routing | Enum | Routing on interface |
| 11 | Filter-ID | String | ACL/filter name |
| 12 | Framed-MTU | Integer | MTU for this connection |
| 13 | Framed-Compression | Enum | Van-Jacobson, etc. |
| 14 | Login-IP-Host | IP | Host for telnet/rlogin |
| 15 | Login-Service | Enum | Telnet, Rlogin, etc. |
| 16 | Login-TCP-Port | Integer | TCP port for login |
| 17 | Reply-Message | String | Display to user |
| 18 | Callback-Number | String | Number for callback |
| 19 | Callback-ID | String | Callback identifier |
| 20 | Framed-Route | String | Route to add |
| 22 | Framed-IPX-Network | IPX | IPX network (legacy) |
| 25 | Class | Binary | Server-to-server opaque data |
| 26 | Vendor-Specific | VSA | Vendor extensions |
| 27 | Session-Timeout | Integer | Max session duration (seconds) |
| 28 | Idle-Timeout | Integer | Idle timeout (seconds) |
| 29 | Termination-Action | Enum | What to do on termination |
| 30 | Called-Station-ID | String | Phone number dialed (or BSSID) |
| 31 | Calling-Station-ID | String | Caller phone number (or MAC) |
| 32 | NAS-Identifier | String | Human-readable NAS name |
| 33 | Proxy-State | Binary | Proxy forwarding data |
| 34 | Login-LAT-Service | String | LAT service |
| 35 | Login-LAT-Node | String | LAT node |
| 36 | Login-LAT-Group | Binary | LAT group |
| 37 | Framed-AppleTalk-Link | Integer | AppleTalk link |
| 38 | Framed-AppleTalk-Network | Integer | AppleTalk network |
| 39 | Framed-AppleTalk-Zone | String | AppleTalk zone |
| 40 | Acct-Status-Type | Enum | Start, Stop, Interim, etc. |
| 41 | Acct-Delay-Time | Integer | Delay in seconds |
| 42 | Acct-Input-Octets | Integer | Bytes received |
| 43 | Acct-Output-Octets | Integer | Bytes sent |
| 44 | Acct-Session-Id | String | Unique session ID |
| 45 | Acct-Authentic | Enum | RADIUS, Local, Remote |
| 46 | Acct-Session-Time | Integer | Session duration |
| 47 | Acct-Input-Packets | Integer | Packets received |
| 48 | Acct-Output-Packets | Integer | Packets sent |
| 49 | Acct-Terminate-Cause | Enum | Why session ended |
| 50 | Acct-Multi-Session-Id | String | Multi-link session |
| 51 | Acct-Link-Count | Integer | Multi-link links |
| 52 | Acct-Input-Gigawords | Integer | High bits of input octets |
| 53 | Acct-Output-Gigawords | Integer | High bits of output octets |
| 61 | NAS-Port-Type | Enum | Ethernet, Wireless, ADSL, etc. |
| 62 | Port-Limit | Integer | Maximum ports |
| 63 | Login-LAT-Port | String | LAT port |
| 64 | Tunnel-Type | Enum | L2TP, PPTP, GRE, etc. |
| 65 | Tunnel-Medium-Type | Enum | IPv4, IPv6, etc. |
| 66 | Tunnel-Client-Endpoint | String | Tunnel client address |
| 67 | Tunnel-Server-Endpoint | String | Tunnel server address |
| 68 | Acct-Tunnel-Connection | String | Tunnel connection ID |
| 69 | Tunnel-Password | Encrypted | Tunnel auth password |
| 70 | ARAP-Password | Binary | AppleTalk password |
| 71 | ARAP-Features | Binary | AppleTalk features |
| 72 | ARAP-Zone-Access | Enum | AppleTalk zone access |
| 73 | ARAP-Security | Integer | AppleTalk security |
| 74 | ARAP-Security-Data | String | AppleTalk security data |
| 75 | Password-Retry | Integer | Retry attempts |
| 76 | Prompt | Enum | Echo/No-Echo |
| 77 | Connect-Info | String | Connection info |
| 78 | Configuration-Token | Binary | Configuration data |
| 79 | EAP-Message | Binary | EAP frame transport |
| 80 | Message-Authenticator | Binary | HMAC-MD5 integrity check |
| 81 | Tunnel-Private-Group-ID | String | Group/VLAN assignment |
| 82 | Tunnel-Assignment-ID | String | Tunnel assignment |
| 83 | Tunnel-Preference | Integer | Tunnel priority |
| 84 | ARAP-Challenge-Response | Binary | AppleTalk challenge |
| 85 | Acct-Interim-Interval | Integer | Interim update interval |
| 86 | Acct-Tunnel-Packets-Lost | Integer | Lost packets |
| 87 | NAS-Port-Id | String | Port name |
| 88 | Framed-Pool | String | IP pool name |
| 90 | Tunnel-Client-Auth-ID | String | Tunnel client auth |
| 91 | Tunnel-Server-Auth-ID | String | Tunnel server auth |
| 95 | NAS-IPv6-Address | IPv6 | NAS IPv6 address |
| 97 | Framed-Interface-Id | Binary | IPv6 interface ID |
| 98 | Framed-IPv6-Prefix | IPv6 | IPv6 prefix |
| 99 | Login-IPv6-Host | IPv6 | Login host IPv6 |
| 100 | Framed-IPv6-Route | String | IPv6 route |
Vendor-Specific Attributes (VSA)
Attribute 26 (Vendor-Specific) is the extensibility mechanism. The format is:
├─ Type=26 (1) ── Length (1) ── Vendor-ID (4) ── Vendor-Type (1) ── Vendor-Length (1) ── Value (varies) ─┤
Each vendor gets a private namespace identified by their IANA enterprise number. Common VSAs:
Cisco (Vendor-ID: 9)
| Vendor-Type | Name | Example |
|---|---|---|
| 1 | Cisco-AVPair | shell:priv-lvl=15 |
| 9 | Cisco-NAS-Port | Port identifier |
| 25 | Cisco-Account-Info | Acct-Input-Octets |
| 41 | Cisco-CLI-Line | Line authorization |
Cisco AV-Pairs are the most flexible — they can encode almost any Cisco device configuration:
# Privilege level authorization
Cisco-AVPair = "shell:priv-lvl=15"
# Download ACL
Cisco-AVPair = "ip:inacl#1=permit tcp any any eq 80"
Cisco-AVPair = "ip:inacl#2=deny ip any any"
# VLAN assignment
Cisco-AVPair = "tunnel-private-group-id=100"
# URL redirect (for web auth)
Cisco-AVPair = "url-redirect=https://captive.example.com"
Cisco-AVPair = "url-redirect-acl=redirect-acl"
Microsoft (Vendor-ID: 311)
| Name | Type | Description |
|---|---|---|
| MS-CHAP-Response | 1 | MS-CHAP response |
| MS-CHAP-Error | 2 | Error message |
| MS-CHAP-CPW-1 | 3 | Change password |
| MS-CHAP-Domain | 10 | User domain |
| MS-MPPE-Encryption-Types | 11 | MPPE encryption |
| MS-MPPE-Encryption-Policy | 12 | MPPE policy |
| MS-MPPE-Send-Key | 16 | Session key |
| MS-MPPE-Recv-Key | 17 | Session key |
| MS-Quarantine-IPFilter | 22 | NAP filter |
| MS-Quarantine-Session-Timeout | 23 | NAP timeout |
| MS-Quarantine-Grade | 25 | NAP grade |
Juniper (Vendor-ID: 4874)
# Static IP assignment
Juniper-Framed-IP-Pool = "pool-name"
# Routing instance
Juniper-Local-Routing-Instance = "customer-a"
# CoS parameters
Juniper-Juniper-Service-Name = "qos-profile"
EAP Methods
RADIUS transports EAP (Extensible Authentication Protocol) frames inside EAP-Message attributes (ID 79). The Message-Authenticator attribute (ID 80) provides integrity protection for the entire RADIUS packet.
EAP-TLS
EAP-TLS uses PKI certificates for mutual authentication. Both client and server present certificates. This is the most secure EAP method — no passwords, no shared secrets (beyond the CA trust chain).
# FreeRADIUS EAP-TLS configuration
# /etc/freeradius/3.0/mods-enabled/eap
eap {
default_eap_type = tls
timer_expire = 60
ignore_unknown_eap_types = no
cisco_accounting_username_bug = no
max_sessions = 4096
tls {
certdir = /etc/freeradius/3.0/certs
private_key_password =
private_key_file = ${certdir}/server.key
certificate_file = ${certdir}/server.pem
ca_file = ${certdir}/ca.pem
dh_file = ${certdir}/dh
random_file = /dev/urandom
fragment_size = 1024
include_length = yes
check_crl = yes
cipher_list = "HIGH:+3DES"
cipher_server_preference = no
ecdh_curve = "prime256v1"
cache {
enable = yes
lifetime = 3600
max_entries = 255
}
verify {
# Require client certificate
require_cert = yes
}
ocsp {
enable = yes
override_cert_url = no
ocsp_default_url = "http://ocsp.example.com"
softfail = no
}
}
}
When to use EAP-TLS:
- Corporate-managed devices (IT can deploy certificates)
- High-security environments (government, finance)
- Environments without AD/domain password infrastructure
PEAP (Protected EAP)
PEAP creates a TLS tunnel, then authenticates the user inside that tunnel using MS-CHAPv2. The server has a certificate; the client authenticates via username/password inside the encrypted tunnel.
# FreeRADIUS PEAP configuration
eap {
default_eap_type = peap
peap {
default_eap_type = mschapv2
copy_request_to_tunnel = no
use_tunneled_reply = no
proxy_tunneled_request_as_eap = yes
virtual_server = "inner-tunnel"
tls {
# Same TLS config as EAP-TLS
certdir = /etc/freeradius/3.0/certs
private_key_file = ${certdir}/server.key
certificate_file = ${certdir}/server.pem
ca_file = ${certdir}/ca.pem
}
}
}
When to use PEAP:
- User-supplied devices (BYOD)
- Domain-joined Windows machines (native support)
- Environments with Active Directory password authentication
EAP-TTLS
Similar to PEAP — creates a TLS tunnel, then authenticates inside it. More flexible than PEAP (supports PAP, CHAP, MS-CHAPv2, or any inner method), but less native OS support.
eap {
default_eap_type = ttls
ttls {
default_eap_type = mschapv2
copy_request_to_tunnel = no
use_tunneled_reply = no
virtual_server = "inner-tunnel"
tls {
certdir = /etc/freeradius/3.0/certs
private_key_file = ${certdir}/server.key
certificate_file = ${certdir}/server.pem
ca_file = ${certdir}/ca.pem
}
}
}
EAP-FAST
Cisco’s alternative to PEAP/TTLS. Uses a shared secret (PAC — Protected Access Credential) instead of a server certificate. Less common today.
Comparison
| Method | Server Cert | Client Cert | Password | OS Support | Security |
|---|---|---|---|---|---|
| EAP-TLS | Required | Required | No | All (config needed) | Highest |
| PEAP | Required | No | Yes (MSCHAPv2) | Native Windows, macOS, Android | High |
| EAP-TTLS | Required | No | Yes (any) | Most | High |
| EAP-FAST | Optional | Optional | Yes | Cisco-focused | Medium |
| LEAP | No | No | Yes (MSCHAPv2) | Legacy | Weak |
FreeRADIUS Configuration
FreeRADIUS is the most widely deployed open-source RADIUS server. This section covers production-grade configuration.
Installation
# Ubuntu/Debian
sudo apt install freeradius freeradius-ldap freeradius-mysql
# RHEL/Fedora
sudo dnf install freeradius freeradius-ldap freeradius-mysql
# Verify installation
sudo radiusd -X # Debug mode (foreground)
Client Configuration
RADIUS clients (NAS devices) are configured in /etc/freeradius/3.0/clients.conf:
# /etc/freeradius/3.0/clients.conf
client wifi-controller {
ipaddr = 10.0.10.0/24
secret = secure-shared-secret-here
shortname = wifi-controller
require_message_authenticator = yes
nas_type = cisco
}
client vpn-gateway {
ipaddr = 10.0.20.5
secret = another-secret
shortname = vpn-gw
nas_type = other
}
# Default client (last resort)
client localhost {
ipaddr = 127.0.0.1
secret = testing123 # CHANGE FOR PRODUCTION
require_message_authenticator = no
}
LDAP Backend
Authenticate users against an LDAP directory (Active Directory, OpenLDAP, FreeIPA):
# /etc/freeradius/3.0/mods-enabled/ldap
ldap {
server = 'dc01.example.com'
port = 389
identity = 'cn=radius,cn=Users,dc=example,dc=com'
password = 'service-account-password'
base_dn = 'dc=example,dc=com'
# Active Directory specific
user {
base_dn = 'cn=Users,dc=example,dc=com'
filter = "(sAMAccountName=%{%{Stripped-User-Name}:-%{User-Name}})"
scope = 'sub'
}
group {
base_dn = 'cn=Users,dc=example,dc=com'
filter = '(objectClass=group)'
membership_attribute = 'memberOf'
}
# TLS
start_tls = yes
tls_cacertfile = /etc/ssl/certs/ca.pem
tls_certfile = /etc/ssl/certs/server.pem
tls_keyfile = /etc/ssl/private/server.key
}
SQL Backend
For environments without LDAP, or when you need fine-grained attribute control:
# /etc/freeradius/3.0/mods-enabled/sql
sql {
driver = "rlm_sql_mysql"
dialect = "mysql"
server = "localhost"
port = 3306
login = "radius"
password = "db-password"
radius_db = "radius"
acct_table1 = "radacct"
authcheck_table = "radcheck"
authreply_table = "radreply"
groupcheck_table = "radgroupcheck"
groupreply_table = "radgroupreply"
usergroup_table = "radusergroup"
}
Create the database schema:
CREATE DATABASE radius;
GRANT ALL ON radius.* TO 'radius'@'localhost' IDENTIFIED BY 'db-password';
USE radius;
CREATE TABLE radcheck (
id int(11) NOT NULL AUTO_INCREMENT,
username varchar(64) NOT NULL,
attribute varchar(64) NOT NULL,
op char(2) NOT NULL DEFAULT '==',
value varchar(253) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE radreply (
id int(11) NOT NULL AUTO_INCREMENT,
username varchar(64) NOT NULL,
attribute varchar(64) NOT NULL,
op char(2) NOT NULL DEFAULT '=',
value varchar(253) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE radacct (
radacctid bigint(20) NOT NULL AUTO_INCREMENT,
acctsessionid varchar(64) NOT NULL,
username varchar(64) NOT NULL,
nasipaddress varchar(15) NOT NULL,
acctstarttime datetime DEFAULT NULL,
acctstoptime datetime DEFAULT NULL,
acctinputoctets bigint(20) DEFAULT NULL,
acctoutputoctets bigint(20) DEFAULT NULL,
PRIMARY KEY (radacctid)
);
# Add users
INSERT INTO radcheck (username, attribute, op, value)
VALUES ('john', 'Cleartext-Password', ':=', 'password123');
# Add authorization attributes
INSERT INTO radreply (username, attribute, op, value)
VALUES ('john', 'Framed-IP-Address', '=', '10.10.10.100'),
('john', 'Session-Timeout', '=', '28800');
Realm Configuration
Realms allow routing different user groups to different backends:
# /etc/freeradius/3.0/proxy.conf
realm example.com {
type = radius
authhost = radius.example.com:1812
accthost = radius.example.com:1813
secret = inter-realm-secret
}
realm local {
type = radius
authhost = localhost:18120
accthost = localhost:18130
secret = local-secret
}
# Default realm
realm NULL {
type = radius
authhost = localhost:1812
accthost = localhost:1813
secret = testing123
}
Virtual Server Configuration
The main policy file is /etc/freeradius/3.0/sites-enabled/default:
server default {
listen {
type = auth
ipaddr = *
port = 0
}
listen {
type = acct
ipaddr = *
port = 0
}
authorize {
preprocess
chap
mschap
suffix
eap {
ok = return
}
files
sql
ldap
expiration
logintime
pap
}
authenticate {
Auth-Type PAP {
pap
}
Auth-Type CHAP {
chap
}
Auth-Type MS-CHAP {
mschap
}
eap
}
preacct {
preprocess
acct_unique
suffix
files
}
accounting {
detail
sql
exec
attr_filter.accounting_response
}
session {
radutmp
sql
}
post-auth {
Post-Auth-Type REJECT {
attr_filter.access_reject
}
}
}
VLAN Assignment
Assign users to specific VLANs via RADIUS attributes:
# For any user matching a specific group
# /etc/freeradius/3.0/mods-config/sql/main/mysql/queries.conf
groupreply {
Tunnel-Type = "VLAN"
Tunnel-Medium-Type = "IEEE-802"
Tunnel-Private-Group-ID = "100"
}
Or per-user in SQL:
INSERT INTO radreply (username, attribute, op, value)
VALUES ('john', 'Tunnel-Type', '=', '13'),
('john', 'Tunnel-Medium-Type', '=', '6'),
('john', 'Tunnel-Private-Group-ID', '=', '100');
Change of Authorization (CoA) and Disconnect
RFC 5176 defines two real-time session management extensions:
Disconnect Messages (DM): Force-terminate an active session.
Change of Authorization (CoA): Modify session parameters mid-session — change VLAN, apply new ACL, limit bandwidth.
# Initiate a disconnect from the RADIUS server
# Using radclient (FreeRADIUS)
echo "User-Name = john, Acct-Session-Id = \"12345678\"" | \
radclient -r 1 -t 3 10.0.10.5:3799 disconnect shared-secret
# Change authorization (apply new VLAN)
echo "User-Name = john, Tunnel-Private-Group-ID = \"200\"" | \
radclient -r 1 -t 3 10.0.10.5:3799 coa shared-secret
Use cases:
- Quarantine a compromised device (CoA → VLAN 999/guest)
- Re-authenticate user after posture change
- Rate-limit abusers mid-session
- Policy-based VLAN steering based on real-time context
Python RADIUS Client (pyrad)
A complete Python implementation for RADIUS authentication and accounting:
pip install pyrad
#!/usr/bin/env python3
"""RADIUS authentication and accounting client using pyrad."""
import pyrad.packet
from pyrad.client import Client, Timeout
from pyrad.dictionary import Dictionary
import socket
import sys
class RadiusClient:
def __init__(self, server, secret, auth_port=1812, acct_port=1813,
timeout=5, retries=3):
self.client = Client(
server=server,
secret=secret.encode(),
dict=Dictionary("dictionary"),
auth_port=auth_port,
acct_port=acct_port,
timeout=timeout,
retries=retries,
)
def authenticate(self, username, password, nas_ip=None,
nas_identifier="NAS-01", service_type=1,
nas_port_type=5):
"""Authenticate a user. Returns (success, reply_attributes)."""
req = self.client.CreateAuthPacket(
code=pyrad.packet.AccessRequest,
User_Name=username,
NAS_Identifier=nas_identifier,
Service_Type=service_type,
NAS_Port_Type=nas_port_type,
)
req["User-Password"] = req.PwCrypt(password)
req["NAS-IP-Address"] = nas_ip or socket.gethostbyname(
socket.gethostname()
)
try:
reply = self.client.SendPacket(req)
except Timeout:
return False, {"error": "RADIUS server timeout"}
except pyrad.client.ServerPacketError as e:
return False, {"error": str(e)}
if reply.code == pyrad.packet.AccessAccept:
attrs = self._parse_reply(reply)
return True, attrs
elif reply.code == pyrad.packet.AccessReject:
return False, {"error": "Access denied"}
elif reply.code == pyrad.packet.AccessChallenge:
return False, {"error": "Challenge required", "eap": True}
return False, {"error": f"Unexpected code: {reply.code}"}
def accounting_start(self, username, session_id, nas_ip=None,
nas_identifier="NAS-01"):
"""Send accounting start packet."""
req = self.client.CreateAcctPacket(
code=pyrad.packet.AccountingRequest,
User_Name=username,
NAS_Identifier=nas_identifier,
Acct_Status_Type=1, # Start
Acct_Session_Id=session_id,
)
req["NAS-IP-Address"] = nas_ip or socket.gethostbyname(
socket.gethostname()
)
try:
reply = self.client.SendPacket(req)
return reply.code == pyrad.packet.AccountingResponse
except Timeout:
return False
def accounting_stop(self, username, session_id, session_time=0,
input_octets=0, output_octets=0,
terminate_cause=1):
"""Send accounting stop packet."""
req = self.client.CreateAcctPacket(
code=pyrad.packet.AccountingRequest,
User_Name=username,
Acct_Status_Type=2, # Stop
Acct_Session_Id=session_id,
Acct_Session_Time=session_time,
Acct_Input_Octets=input_octets,
Acct_Output_Octets=output_octets,
Acct_Terminate_Cause=terminate_cause,
)
try:
reply = self.client.SendPacket(req)
return reply.code == pyrad.packet.AccountingResponse
except Timeout:
return False
def _parse_reply(self, reply):
"""Convert RADIUS reply attributes to a dict."""
attrs = {}
for attr in reply.keys():
try:
val = reply[attr]
if isinstance(val, list):
val = [str(v) for v in val]
else:
val = str(val)
attrs[attr] = val
except Exception:
continue
return attrs
if __name__ == "__main__":
client = RadiusClient(
server="127.0.0.1",
secret="testing123",
)
success, result = client.authenticate("john", "password123")
if success:
print(f"Authenticated: {result}")
else:
print(f"Failed: {result}")
sys.exit(1)
Accounting Deep Dive
RADIUS accounting tracks user sessions for billing, auditing, and capacity planning.
Accounting Flow
1. NAS sends Accounting-Request (Start) — session begins
2. Server responds Accounting-Response — acknowledged
3. NAS sends Accounting-Request (Interim) — periodic (every N minutes)
4. Server responds Accounting-Response — acknowledged
5. NAS sends Accounting-Request (Stop) — session ends
6. Server responds Accounting-Response — acknowledged
Accounting Attributes
# Typical accounting packet
Acct-Status-Type = Interim-Update (3)
Acct-Session-Id = "550E8400-E29B-41D4-A716-446655440000"
Acct-Input-Octets = 1048576
Acct-Output-Octets = 2097152
Acct-Input-Packets = 15000
Acct-Output-Packets = 25000
Acct-Session-Time = 3600
Acct-Input-Gigawords = 0
Acct-Output-Gigawords = 0
NAS-IP-Address = 10.0.10.5
NAS-Identifier = "wifi-controller-01"
User-Name = "john"
FreeRADIUS Accounting Queries
-- Get total data usage per user (last 30 days)
SELECT username,
SUM(acctinputoctets + acctoutputoctets) AS total_bytes,
ROUND(SUM(acctinputoctets + acctoutputoctets) / 1073741824, 2) AS total_gb,
COUNT(*) AS sessions,
ROUND(SUM(acctsessiontime) / 3600, 1) AS total_hours
FROM radacct
WHERE acctstarttime >= NOW() - INTERVAL 30 DAY
GROUP BY username
ORDER BY total_bytes DESC;
-- Active sessions right now
SELECT username, nasipaddress, acctstarttime, acctinputoctets, acctoutputoctets
FROM radacct
WHERE acctstoptime IS NULL;
-- Peak concurrent usage by hour
SELECT DATE_FORMAT(acctstarttime, '%Y-%m-%d %H:00:00') AS hour_slot,
COUNT(*) AS concurrent
FROM radacct
WHERE acctstarttime >= NOW() - INTERVAL 7 DAY
GROUP BY hour_slot
ORDER BY concurrent DESC
LIMIT 10;
RadSec (RADIUS over TLS)
Standard RADIUS sends packets over UDP with only MD5-based integrity — no encryption, no confidentiality. RadSec (RFC 6614) tunnels RADIUS over TLS, solving:
- Confidentiality: Full encryption of all attributes (including User-Password)
- Integrity: TLS-level message authentication
- Reliability: TCP retransmission instead of application-level retry
- Dynamic peers: No need to pre-configure shared secrets for inter-domain roaming
FreeRADIUS RadSec Configuration
# /etc/freeradius/3.0/sites-enabled/tls
listen {
type = auth
proto = tcp
tls {
certdir = /etc/freeradius/3.0/certs
private_key_file = ${certdir}/server.key
certificate_file = ${certdir}/server.pem
ca_file = ${certdir}/ca.pem
}
}
# RadSec client
client radsec-peer {
ipaddr = 10.0.30.0/24
proto = tls
secret = # Not needed when using TLS client certs
shortname = radsec-peer
}
When to Use RadSec
- RADIUS traffic across the internet or untrusted networks
- Federated eduroam deployments
- Multi-site enterprise with inter-controller roaming
- Compliance requirements (PCI-DSS, HIPAA, FedRAMP)
Troubleshooting
Debug Mode
FreeRADIUS’s -X flag runs in full debug mode — the most valuable troubleshooting tool:
# Stop the service, run in foreground debug
sudo systemctl stop freeradius
sudo radiusd -X
Look for these key patterns in the debug output:
# Successful authentication
rlm_pap: Login OK: [john] (from client wifi-controller)
Login OK: [john] (from client wifi-controller port 0)
# Attributes sent back:
Sending Access-Accept of id 123 to 10.0.10.5 port 1645
Framed-IP-Address = 10.10.10.100
Session-Timeout = 28800
# Failed authentication
rlm_pap: Login incorrect: [john] (from client wifi-controller)
Sending Access-Reject of id 123 to 10.0.10.5 port 1645
Reply-Message = "Authentication failed"
Common Issues
NAS sends request but gets no response
# Check firewall
sudo iptables -L -n | grep 1812
sudo ufw status
# Verify client is configured
grep -A5 "client wifi" /etc/freeradius/3.0/clients.conf
# Test packet reachability
radtest john password123 127.0.0.1 0 testing123
EAP authentication fails
# Common causes:
# 1. Certificate issues — check expiration
openssl x509 -in /etc/freeradius/3.0/certs/server.pem -text -noout
# 2. Wrong EAP type — ensure client and server agree
# 3. Inner method mismatch — PEAP expects MSCHAPv2 inside
# 4. TLS version mismatch — configure ciphers
tail -f /var/log/freeradius/radius.log | grep -i "eap\|tls\|ssl"
Accounting data missing
# Check if accounting is enabled in the NAS
# Verify SQL connection
sudo radiusd -X | grep sql
# Test accounting
radacct -d /etc/freeradius/3.0/ -n 1
Shared secret mismatch
# FreeRADIUS debug output:
# "Ignoring request to authentication port"
# "Request from unknown client IP"
# Fix: verify secret in clients.conf matches NAS config
# Always restart after changing secrets
Fragmentation issues with EAP
Large EAP messages (especially certificate chains) may exceed UDP packet limits:
# Configure EAP fragment size
eap {
tls {
fragment_size = 1024 # Max fragment size
include_length = yes # Include total length
}
}
Monitoring Commands
# Check RADIUS server status
sudo systemctl status freeradius
sudo radiusd -C # Configuration check
# View live authentication attempts
sudo tail -f /var/log/freeradius/radius.log
# Count recent authentications
grep -c "Login OK" /var/log/freeradius/radius.log
grep -c "Login incorrect" /var/log/freeradius/radius.log
# Check active sessions
sudo radwho
# Verify packet counters
sudo radiusd -C -l stdout
Security Considerations
Known Weaknesses
-
User-Password encryption: Uses MD5 XOR with the shared secret — reversible if you know the secret, and susceptible to offline dictionary attacks on captured packets.
-
No built-in confidentiality: Attributes like Tunnel-Password, MS-MPPE-Send-Key, and MS-MPPE-Recv-Key are encrypted with weak algorithms.
-
Request authentication not mandatory: The Message-Authenticator attribute (ID 80) is optional in older implementations — without it, packets can be forged.
-
UDP amplification: RADIUS over UDP can be used in reflection attacks if the server is exposed to the internet.
Hardening Checklist
- Use EAP-TLS or PEAP instead of PAP/CHAP (passwords never transmitted in the clear)
- Require
Message-Authenticatoron all packets (require_message_authenticator = yesin clients.conf) - Tunnel RADIUS over RadSec or IPsec for inter-site traffic
- Rotate shared secrets periodically (store in a secret manager)
- Restrict RADIUS server access to NAS IP ranges only
- Use strong secrets (>20 characters, random)
- Enable TLS for LDAP backend connections
- Log all authentication attempts and monitor for anomalies
- Deploy redundant RADIUS servers (active-active with load balancing)
- Disable Status-Server if not needed (reduces reconnaissance surface)
Network Segmentation
Internet
│
┌─────────▼─────────┐
│ Firewall (deny │
│ RADIUS from WAN) │
└─────────┬─────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌─────▼─────┐ ┌────▼────┐ ┌──────▼─────┐
│ RADIUS │ │ RADIUS │ │ LDAP / AD │
│ Server 1 │ │ Server 2│ │ Domain Ctrl │
│ (mgmt) │ │ (mgmt) │ │ (mgmt) │
└─────┬─────┘ └────┬────┘ └──────┬──────┘
│ │ │
┌─────▼───────────────▼───────────────▼──────┐
│ Management VLAN (10.x.x.x) │
└─────────────────────────────────────────────┘
│
┌─────▼─────┐
│ NAS/WiFi │
│ Controller│
└───────────┘
Performance Tuning
FreeRADIUS Performance
# /etc/freeradius/3.0/radiusd.conf
thread_pool {
start_servers = 5
max_servers = 32
min_spare_servers = 3
max_spare_servers = 10
max_requests_per_server = 0
autoscale = yes
}
# Increase UDP buffer sizes
# /etc/sysctl.d/99-radius.conf
net.core.rmem_max = 16777216
net.core.rmem_default = 1048576
net.core.wmem_max = 16777216
net.core.wmem_default = 1048576
Benchmarking
# Install radclient for benchmarking
# Test authentication throughput
time seq 1 1000 | parallel -j 10 \
'echo "User-Name = test{}-password:testing123" | \
radclient -r 1 -t 2 127.0.0.1:1812 auth testing123'
# Monitor server performance
sudo radiusd -C -l stdout | grep -e "requests" -e "average"
Expected performance (modern hardware):
- PAP authentication: 5,000–10,000 req/s per core
- PEAP/MSCHAPv2: 500–1,000 auth/s per core
- EAP-TLS (full handshake): 100–300 auth/s per core
- Accounting: 10,000–20,000 req/s per core
Conclusion
RADIUS remains the foundation of enterprise network access control in 2026. Despite its age, the protocol’s extensibility via attributes, support for modern EAP methods, and proxy architecture keep it relevant for WiFi authentication, VPN access, network device administration, and IoT onboarding.
Key takeaways:
- Use EAP methods with TLS — PAP/CHAP passwords are transmitted with weak encryption
- Deploy redundantly — RADIUS is a single point of failure; use at least two servers
- Monitor aggressively — authentication failures often indicate attacks or misconfiguration
- Tunnel RADIUS over RadSec/IPsec for inter-site traffic and cloud-based RADIUS services
- Invest in FreeRADIUS — it’s the most capable open-source RADIUS server, with LDAP, SQL, EAP, CoA, and proxy support built in
Resources
- RFC 2865 - RADIUS Authentication/Authorization
- RFC 2866 - RADIUS Accounting
- RFC 5176 - Dynamic Authorization (CoA/DM)
- RFC 6614 - RADIUS over TLS (RadSec)
- FreeRADIUS Documentation
- FreeRADIUS WiKi
- EAP Method Reference (RFC 3748)
- pyrad Python Library
- Network RADIUS (RadSec commercial)
Comments