yum upgrades for production use, this is the repository for you.
Active subscription is required.
Stock NGINX cannot authenticate users against RADIUS; the dynamic nginx-module-auth-radius package from the GetPageSpeed repository enables NGINX RADIUS authentication per RFC 2865, so FreeRADIUS, Microsoft NPS, Cisco ISE, or Aruba ClearPass can decide who gets through your proxy.
If your organization already runs a AAA server, you know the pain this solves. The network team keeps every credential in RADIUS. Meanwhile, each internal web tool behind NGINX grows its own htpasswd file that someone must update on every hire, departure, and password change. Those files drift, ex-employees keep working passwords, and security audits flag every copy. NGINX RADIUS authentication removes the duplicate credential store entirely: the browser shows a standard login prompt, and your existing RADIUS server gives the verdict.
In this guide, we install the module from the GetPageSpeed repository, wire it to a real FreeRADIUS server, and verify every scenario with curl: accept, reject, failover, and health checks. We also cover a BlastRADIUS-era compatibility trap that silently breaks this setup on modern FreeRADIUS versions.
How It Works
The module registers at the NGINX access phase, exactly like the built-in auth_basic machinery. The flow for each protected request:
- NGINX asks the client for HTTP Basic credentials by returning
401with aWWW-Authenticate: Basic realm="..."header. - The browser resends the request with the
Authorizationheader. The module extracts the username and password. - The module builds an RFC 2865 Access-Request packet. Therefore, the password never travels in cleartext to the RADIUS server: it uses the standard PAP User-Password encoding, an XOR against an MD5 keystream derived from the shared secret and request authenticator.
- The packet goes to your RADIUS server over UDP (port 1812 by default). An Access-Accept lets the request through; an Access-Reject produces
401again. - If a RADIUS server does not respond within the timeout budget or refuses the connection, the module fails over to the next configured server. When no servers are left, NGINX returns
503.
The implementation is fully non-blocking. Each configured server gets a pool of persistent UDP sockets (the queue_size directive, default 10), created once per worker, so authentication does not spawn connections under load. Responses are verified against the RFC 2865 Response Authenticator, so a spoofed or corrupted reply is discarded rather than trusted.
Two design facts worth knowing up front. First, the module speaks PAP only: there is no CHAP or MS-CHAP support in the code. Second, it performs no caching of authentication results, so every protected request costs one RADIUS round trip. Both facts shape the performance and security advice later in this article.
When to Use RADIUS Instead of Native Auth
NGINX already ships auth_basic (static password files) and can front LDAP or Kerberos with other modules from our repository. Choose NGINX RADIUS authentication when the credential authority in your organization is already a RADIUS/AAA deployment:
- Wi-Fi and VPN credentials in FreeRADIUS or Microsoft NPS that should also gate internal dashboards
- Network gear authentication through Cisco ISE or Aruba ClearPass that you want to extend to HTTP services
- Any policy engine that can express its decision as RADIUS Access-Accept or Access-Reject, including per-user rules, time-of-day restrictions, and one-time-password backends that ride on RADIUS
For LDAP-first or Active Directory-first shops without RADIUS, see our NGINX LDAP authentication and NGINX Kerberos authentication guides instead. The PAM module is another route, however it calls PAM synchronously, while the RADIUS module stays event-driven.
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux
Install the GetPageSpeed release package, then the module:
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-auth-radius
Then load the module at the very top of /etc/nginx/nginx.conf:
load_module modules/ngx_http_auth_radius_module.so;
The package tracks our NGINX builds, so the module binary always matches your nginx version. See the auth-radius module page for supported platforms.
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-auth-radius
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
The APT module page lists available Debian and Ubuntu releases.
Configuration
The module provides five directives. radius_server declares a named server at the http level; the remaining directives activate authentication inside a location.
| Directive | Context | Purpose | Default |
|---|---|---|---|
radius_server "name" { ... } |
http |
Declares a named RADIUS server (block) | required |
radius_servers "name" |
location |
Attaches a declared server; repeat the directive for failover order | required |
auth_radius "realm" | off |
location |
Enables authentication with the given Basic realm | disabled |
radius_auth |
location |
Alias of auth_radius |
disabled |
radius_health [user] [passwd] |
location |
Turns the location into a RADIUS health probe | disabled |
Inside a radius_server block:
| Key | Purpose | Default |
|---|---|---|
url |
host:port of the RADIUS server |
required; port defaults to 1812 |
secret |
RADIUS shared secret | required |
nas_identifier |
Optional NAS-Identifier attribute (3-64 characters) | omitted |
auth_timeout |
Wait per authentication attempt | 5s |
auth_retries |
Resends before the server is considered down | 3 |
health_timeout |
Wait per health-probe attempt | 5s |
health_retries |
Resends for health probes | 1 |
queue_size |
Concurrent in-flight requests per server (1-255) | 10 |
A complete working example, exactly as tested on Rocky Linux 10 with nginx 1.30.4:
# /etc/nginx/conf.d/radius-auth.conf
radius_server "corp_radius" {
url "127.0.0.1:1812";
secret "testing123";
nas_identifier "web-frontend";
auth_timeout 5s;
auth_retries 3;
}
server {
listen 8081;
server_name _;
root /usr/share/nginx/html;
location / {
radius_servers "corp_radius";
auth_radius "Restricted Area";
}
}
Validate and reload:
sudo nginx -t
sudo systemctl reload nginx
Use a real shared secret in production, of course. Additionally, keep the quotes: values with special characters parse predictably that way.
Testing NGINX RADIUS Authentication
We verified each scenario against a live FreeRADIUS server with a testuser account. Without credentials, the module challenges the client:
curl -sI http://localhost:8081/ | head -2
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Restricted Area"
With valid credentials, RADIUS answers Access-Accept and the request goes through:
curl -s -o /dev/null -w "%{http_code}\n" -u "testuser:S3cureP@ss" http://localhost:8081/
200
A wrong password produces Access-Reject, and the module returns 401 so the browser can prompt again:
curl -s -o /dev/null -w "%{http_code}\n" -u "testuser:WRONGPASS" http://localhost:8081/
401
Finally, when every configured RADIUS server is unreachable, NGINX fails closed with 503 rather than letting requests through unauthenticated.
The BlastRADIUS Trap on Modern FreeRADIUS
This is the part that will save you an afternoon. Since the BlastRADIUS vulnerability (CVE-2024-3596), FreeRADIUS 3.2.5 and later track whether a client has ever sent the Message-Authenticator attribute. The moment one request from your NGINX host includes it, FreeRADIUS locks that client into requiring the attribute on every subsequent packet.
Here is the trap: the radtest utility always sends Message-Authenticator. So if you sanity-check your RADIUS server with radtest first, and then point NGINX at it, FreeRADIUS silently drops the module’s requests, because they carry no Message-Authenticator. The symptom is maddening: radtest works, yet NGINX returns 503 after exactly auth_timeout times auth_retries seconds, and nothing appears in the RADIUS log.
The fix is explicit client configuration. In /etc/raddb/clients.conf (FreeRADIUS), pin the behavior for the NGINX host:
client webfrontend {
ipaddr = 10.0.0.5
secret = testing123
require_message_authenticator = false
}
Then restart FreeRADIUS. Because this loosens one BlastRADIUS mitigation for that specific client, treat the network path between NGINX and RADIUS as sensitive: keep it on localhost, a dedicated VLAN, or inside an IPsec/WireGuard tunnel. That advice is not new, since PAP over UDP has always assumed a trusted transport.
Failover and Health Checks
Declare several servers and list them in priority order by repeating radius_servers. The module advances to the next server on timeout or connection refusal:
radius_server "primary_radius" {
url "10.0.0.10:1812";
secret "testing123";
auth_timeout 2s;
auth_retries 1;
}
radius_server "backup_radius" {
url "10.0.0.11:1812";
secret "testing123";
}
server {
listen 8081;
root /usr/share/nginx/html;
location /failover/ {
radius_servers "primary_radius";
radius_servers "backup_radius";
auth_radius "Restricted Area";
}
}
In our test, with the primary deliberately dead, authentication still returned 200 in under five milliseconds, because a refused UDP port fails over instantly. Only genuine packet loss costs the full auth_timeout per retry, so keep auth_timeout and auth_retries low on the primary when a backup exists.
The radius_health directive turns a location into a liveness probe for your monitoring system. Any RADIUS response, Accept or Reject alike, counts as healthy; only an unreachable server fails the probe:
location = /radius-health {
radius_servers "corp_radius";
radius_health "healthprobe" "irrelevant";
try_files /health.txt =404;
}
With FreeRADIUS up, this returned 200; with it stopped, 503. One caveat we hit while testing: do not implement the health endpoint with return 200. The return directive short-circuits the access phase, so the RADIUS probe would never run and the endpoint would report healthy forever.
Performance Considerations
The module adds one RADIUS round trip to every protected request; there is no result cache, not even for repeated requests with identical credentials. On a LAN this costs a millisecond or two, which is fine for admin panels and internal tools. Consequently, for high-traffic protected paths you should plan capacity:
queue_sizebounds concurrency. Each server processes at mostqueue_size(default 10, maximum 255) simultaneous authentications per worker; excess requests wait on a 100 ms retry timer. Raise it if you protect busy endpoints.- Sockets are persistent. UDP sockets are created once per worker at startup, so there is no per-request connection cost and nothing to tune in
worker_connectionsterms. - Scope the protection. Protect
location /admin/rather thanlocation /, and let static assets bypass authentication entirely. - Timeout budget is multiplicative. The worst-case delay for a down server is
auth_timeouttimesauth_retriesper server in the failover chain. The defaults give 15 seconds per server; tighten them where a backup exists.
Security Best Practices
- Always front with HTTPS. The browser leg is HTTP Basic authentication, so without TLS the password crosses the wire base64-encoded, which is to say naked. Terminate TLS in the same server block that enforces
auth_radius. - Treat the RADIUS leg as sensitive. PAP’s User-Password encoding is MD5-based obfuscation from 2000, not modern cryptography. Keep RADIUS traffic on a trusted segment or inside a tunnel, and use long, random shared secrets, since the secret is the only key material.
- Remember the module is PAP-only. If your RADIUS policy requires CHAP or MS-CHAP, or EAP for that matter, this module cannot satisfy it; PAP must be an allowed authentication type for the NAS client.
- SELinux needs two switches on RHEL-family systems. Allow outbound connections from NGINX with
setsebool -P httpd_can_network_connect on, and if you serve on a non-standard port like 8081, label it:semanage port -m -t http_port_t -p tcp 8081. We hit both on a stock Rocky Linux 10 install. - Open UDP 1812 outbound from the NGINX host to the RADIUS servers in your firewall; replies come back on the same flow, so no inbound rule is needed with stateful filtering.
Troubleshooting
503 with valid credentials, nothing in the RADIUS log. Almost always the BlastRADIUS client lock described above, particularly when radtest succeeds from the same host. Set require_message_authenticator = false for the NGINX client and restart the RADIUS server.
503 after exactly 15 seconds. That is the default auth_timeout 5s times auth_retries 3: packets go out but no reply returns. Check firewall rules for UDP 1812, verify the RADIUS server is listening on the expected address, and confirm SELinux booleans. A wrong shared secret can also land here: the module discards replies whose Response Authenticator fails verification, which looks identical to packet loss. tcpdump -i any -n udp port 1812 tells the two cases apart in seconds: no replies means network or client lock, replies arriving but still 503 means the secret.
unknown directive "radius_server". The module is not loaded. On RHEL-family systems add the load_module line at the very top of nginx.conf, outside every block; our nginx.org-style layout does not auto-include module configuration files.
Health endpoint always returns healthy. You used return in the health location; it short-circuits the access phase before the probe runs. Serve a small static file with try_files instead, as shown above.
Everything correct but still failing after config moves. If you staged files under /tmp and moved them into /etc, they may carry a wrong SELinux context. Run restorecon -Rv /etc/nginx (or /etc/raddb on the RADIUS side); we hit exactly this while preparing this guide.
Conclusion
A working RADIUS gate is only as reliable as its next config change. One rearranged location block or a forgotten realm quietly reopens paths you meant to protect. GetPageSpeed Amplify runs scheduled gixy scans across every host and ties findings to live NGINX runtime metrics. Drop-in compatible with the deprecated nginx-amplify-agent (EOL January 2026).
The nginx-module-auth-radius package turns NGINX into a proper RADIUS NAS: standard browser login, RFC 2865 PAP with response verification, multi-server failover, and health probing, all in an event-driven module that matches your installed NGINX version exactly. Every NGINX RADIUS authentication scenario in this article was verified end to end on a clean Rocky Linux 10 machine against FreeRADIUS, including the failure modes.
The module ships in the GetPageSpeed Premium Repository alongside more than 140 other NGINX modules, from TOTP two-factor authentication to NTLM and Shibboleth, each maintained and rebuilt for every NGINX release so your authentication layer never breaks on upgrade. Subscribe to the repository to install any of them with a single dnf or apt-get command.
Source code: dvershinin/ngx_http_auth_radius_module on GitHub.
