Site icon GetPageSpeed

NGINX RADIUS Authentication: No More htpasswd Files

NGINX RADIUS Authentication: Central AAA Without htpasswd Files

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:

  1. NGINX asks the client for HTTP Basic credentials by returning 401 with a WWW-Authenticate: Basic realm="..." header.
  2. The browser resends the request with the Authorization header. The module extracts the username and password.
  3. 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.
  4. The packet goes to your RADIUS server over UDP (port 1812 by default). An Access-Accept lets the request through; an Access-Reject produces 401 again.
  5. 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:

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_module directive 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:

Security Best Practices

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.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version