yum upgrades for production use, this is the repository for you.
Active subscription is required.
NGINX ships with basic authentication built in, but it has a well-known weakness: passwords are sent as Base64 text with every request. Anyone who intercepts the traffic can decode them instantly. NGINX digest authentication solves this by using a challenge-response mechanism that never transmits the actual password over the wire.
The ngx_http_auth_digest module implements RFC 2617 Digest Access Authentication for NGINX. Instead of sending your password in plain text, the server issues a one-time challenge (called a nonce), and the client responds with an MD5 hash that proves it knows the password without revealing it. This makes digest auth significantly more resistant to packet-sniffing attacks than basic auth.
In this guide, you will learn how to install and configure NGINX digest authentication on Rocky Linux, AlmaLinux, and other Enterprise Linux distributions. We will cover every directive, explain how to create password files, and show you how the built-in brute-force protection works.
How Digest Authentication Works in NGINX
Understanding the authentication flow helps you configure the module correctly and troubleshoot issues effectively.
The Challenge-Response Flow
Here is what happens when a client requests a protected resource:
- Client sends a request without credentials
- Server responds with 401 Unauthorized and includes a
WWW-Authenticateheader containing a random nonce, the realm name, and the supported algorithm (MD5) - Client computes a digest response by hashing the username, realm, password, nonce, request method, and URI together
- Client sends the request again with an
Authorizationheader containing the computed digest - Server verifies the digest by performing the same calculation using the stored password hash
- Server responds with 200 OK and includes an
Authentication-Infoheader that the client can use to verify the server’s identity
The key security advantage is in step 3: the client never sends the raw password. Even if an attacker captures the Authorization header, they cannot extract the password from the hash without a brute-force attack.
What the Headers Look Like
When you access a protected location, the server sends this challenge:
WWW-Authenticate: Digest algorithm="MD5", qop="auth", realm="private", nonce="3cd3456e6987556b"
The client then responds with:
Authorization: Digest username="admin", realm="private", nonce="3cd3456e6987556b",
uri="/private/", cnonce="QOwYKVJ/Q09GFV8c", nc=00000001, qop=auth,
response="64cc593843cff4153e5c76c458543850", algorithm=MD5
The nc (nonce count) field increments with each request using the same nonce, which prevents replay attacks. The cnonce (client nonce) adds additional randomness to the digest computation.
Digest Auth vs. Basic Auth
Before diving into configuration, here is a comparison to help you decide which authentication method fits your use case:
| Feature | Basic Auth | Digest Auth |
|---|---|---|
| Password transmission | Base64-encoded (readable) | MD5 hash (not readable) |
| Sniffing resistance | None without TLS | Resists passive sniffing |
| Replay protection | None | Nonce counting prevents replays |
| Brute-force protection | None built-in | Built-in lockout after failed attempts |
| Browser support | Universal | All modern browsers |
| Password file format | htpasswd | htdigest |
$remote_user variable |
Set automatically | Not set by this module |
| Setup complexity | Minimal | Moderate |
When to use NGINX digest authentication:
- Internal networks where TLS is not enforced everywhere
- Legacy systems that cannot use JWT or TOTP
- Environments where you need brute-force protection without additional tools
- Situations where passwords must not appear in access logs (basic auth leaks to
$remote_user)
When to use basic auth instead:
- You already enforce HTTPS everywhere (TLS encrypts the password)
- You need
$remote_userfor logging or downstream headers - You want the simplest possible setup
For maximum security, consider combining digest auth with IP-based access control or upgrading to JWT authentication for API endpoints.
Installation
The ngx_http_auth_digest module is available as a pre-built dynamic module from the GetPageSpeed repository. No compilation is required.
RHEL, Rocky Linux, AlmaLinux, and CentOS
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-auth-digest
Then enable the module by adding this line at the top of /etc/nginx/nginx.conf, before the events block:
load_module modules/ngx_http_auth_digest_module.so;
Debian and Ubuntu
Set up the APT repository, then:
sudo apt-get update
sudo apt-get install nginx-module-auth-digest
On Debian and Ubuntu, the module is automatically enabled by the package. You do not need to add the load_module directive manually.
Verify the Module is Loaded
After adding the load_module directive (on RHEL-based systems), test your configuration:
sudo nginx -t
You should see:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Creating the Password File
NGINX digest authentication uses the htdigest password file format. Each line contains a username, realm, and an MD5 hash of all three combined with the password.
File Format
The password file follows this format:
username:realm:MD5(username:realm:password)
For example, for user admin in realm private with password secret123:
admin:private:7552e1614f8d342d2c10d9369e0cf530
Creating the File with htdigest
If you have Apache’s htdigest utility installed:
sudo htdigest -c /etc/nginx/.digest_pw private admin
The -c flag creates a new file. Omit it when adding additional users to an existing file:
sudo htdigest /etc/nginx/.digest_pw private editor
Creating the File with OpenSSL
If htdigest is not available, you can generate the hash manually:
# Generate the hash
echo -n "admin:private:secret123" | md5sum | awk '{print $1}'
Then create the file:
echo "admin:private:$(echo -n 'admin:private:secret123' | md5sum | awk '{print $1}')" | sudo tee /etc/nginx/.digest_pw
Set Proper Permissions
The password file must be readable by the NGINX worker process:
sudo chmod 640 /etc/nginx/.digest_pw
sudo chown root:nginx /etc/nginx/.digest_pw
Security note: Never place the password file inside your web root. Store it in /etc/nginx/ or another directory that is not served by NGINX.
Basic Configuration
Here is a minimal configuration that protects a location with NGINX digest authentication:
auth_digest_shm_size 4m;
server {
listen 80;
server_name example.com;
root /var/www/html;
location /private/ {
auth_digest 'private';
auth_digest_user_file /etc/nginx/.digest_pw;
}
}
The auth_digest directive takes a realm name as its argument. This realm must match the realm used when creating the password file. If they do not match, authentication will always fail.
Disabling Auth for Sub-paths
You can selectively disable authentication for specific sub-paths:
location /private/ {
auth_digest 'private';
auth_digest_user_file /etc/nginx/.digest_pw;
location /private/public/ {
auth_digest off;
}
}
In this configuration, /private/ requires authentication, but /private/public/ is accessible to everyone.
Complete Directive Reference
The module provides 9 configuration directives. Here is the complete reference with explanations and recommended values.
auth_digest
Enables or disables digest authentication for a location and sets the realm name.
Syntax: auth_digest [realm-name | off]
Default: off
Context: http, server, location, limit_except
location /admin/ {
auth_digest 'admin-area';
auth_digest_user_file /etc/nginx/.digest_pw;
}
The realm name appears in the browser’s authentication dialog and must exactly match the realm in your htdigest password file.
auth_digest_user_file
Specifies the path to the htdigest-format password file.
Syntax: auth_digest_user_file /path/to/file
Default: none
Context: http, server, location, limit_except
This directive supports NGINX variables, which allows you to use different password files per virtual host:
auth_digest_user_file /etc/nginx/digest/$host.digest;
auth_digest_timeout
Sets how long a nonce remains valid for the initial authentication challenge.
Syntax: auth_digest_timeout time
Default: 60s
Context: http, server, location
When a client receives a 401 challenge, it has this many seconds to respond with valid credentials. If the nonce expires before the client responds, the server issues a new challenge with stale=true, which tells the browser to retry transparently without prompting the user again.
auth_digest_timeout 60s;
Recommendation: The default of 60 seconds works well for most scenarios. Increase it only if clients are on very slow connections.
auth_digest_expires
Controls how long a nonce can be reused after the first successful authentication.
Syntax: auth_digest_expires time
Default: 10s
Context: http, server, location
After a client authenticates successfully with a nonce, that nonce remains valid for subsequent requests during this window. Once expired, the server issues a new nonce with stale=true.
auth_digest_expires 10s;
Recommendation: Keep this short (10-30 seconds) for security. Longer values reduce the number of authentication round-trips but increase the replay attack window.
auth_digest_replays
Sets the maximum number of times a single nonce can be used.
Syntax: auth_digest_replays number
Default: 20
Context: http, server, location
Each request increments the nonce counter (nc). Once this limit is reached, the client must obtain a new nonce. This prevents indefinite reuse of a captured nonce.
auth_digest_replays 20;
Recommendation: The default of 20 is suitable for most applications. A typical page load involves 1-5 authenticated requests. Reduce this value for higher-security environments.
This value also affects shared memory usage: each nonce entry uses 48 + ceil(replays / 8) bytes. With the default of 20, each entry uses 51 bytes.
auth_digest_maxtries
Sets the maximum number of failed authentication attempts before locking out the client IP.
Syntax: auth_digest_maxtries number
Default: 5
Context: http, server, location
auth_digest_maxtries 5;
After 5 failed attempts from the same IP address, the server immediately returns 401 without even evaluating the credentials. This built-in brute-force protection is one of the key advantages of digest auth over basic auth.
auth_digest_evasion_time
Sets how long a locked-out client IP remains blocked after exceeding auth_digest_maxtries.
Syntax: auth_digest_evasion_time time
Default: 300s
Context: http, server, location
auth_digest_evasion_time 300s;
During the evasion period, the server does not even send a WWW-Authenticate challenge header. It returns a bare 401 response, which means the client cannot attempt authentication at all until the lockout expires.
Recommendation: The default of 5 minutes provides a good balance. For high-security environments, increase this to 600s or more.
auth_digest_drop_time
Controls when expired nonce entries are removed from shared memory.
Syntax: auth_digest_drop_time time
Default: 300s
Context: http, server, location
This is a housekeeping directive. After a nonce expires (via auth_digest_expires), its entry stays in shared memory for drop_time seconds before being cleaned up. This prevents memory churn from rapid nonce creation and deletion.
auth_digest_drop_time 300s;
Recommendation: The default is fine for most deployments. Only reduce this if you are constrained on shared memory.
auth_digest_shm_size
Sets the size of the shared memory zone used to track active nonces and evasion state.
Syntax: auth_digest_shm_size size
Default: 4m (4 megabytes)
Context: http, server
This directive can only be set at the http or server level, not inside location blocks.
auth_digest_shm_size 4m;
Important: If shared memory is exhausted, the module returns 503 Service Unavailable for all authentication requests until nonces expire and free up space. Therefore, you must size this zone appropriately for your traffic.
Sizing the Shared Memory Zone
Use this formula to calculate the required size:
required_bytes = concurrent_auth_requests * (48 + ceil(replays / 8))
With default settings (replays=20, timeout=60s, expires=10s):
- Each nonce uses approximately 51 bytes
- A nonce lives for up to
timeout + drop_time= 360 seconds in the worst case - At 100 requests/second: 100 * 360 * 51 = ~1.8 MB
The default 4 MB can handle roughly 82,000 concurrent nonces, which is sufficient for most deployments.
Production Configuration Example
Here is a complete production-ready configuration with NGINX digest authentication tuned for security:
load_module modules/ngx_http_auth_digest_module.so;
events {
worker_connections 1024;
}
http {
auth_digest_shm_size 4m;
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/pki/tls/certs/example.com.crt;
ssl_certificate_key /etc/pki/tls/private/example.com.key;
root /var/www/html;
# Protected admin area
location /admin/ {
auth_digest 'admin';
auth_digest_user_file /etc/nginx/.digest_admin;
auth_digest_timeout 60s;
auth_digest_expires 10s;
auth_digest_replays 10;
auth_digest_maxtries 3;
auth_digest_evasion_time 600s;
auth_digest_drop_time 300s;
}
# Protected API with relaxed replay limit
location /api/internal/ {
auth_digest 'api';
auth_digest_user_file /etc/nginx/.digest_api;
auth_digest_replays 50;
auth_digest_timeout 120s;
auth_digest_expires 30s;
}
# Public content
location / {
try_files $uri $uri/ =404;
}
}
}
In this example, the admin area uses stricter brute-force protection (3 attempts, 10-minute lockout), while the internal API allows more nonce reuses to accommodate automated clients that make many requests.
Testing Your Configuration
Verify Config Syntax
Always test after making changes:
sudo nginx -t && sudo systemctl reload nginx
Test with curl
Use curl’s --digest flag to test digest authentication from the command line:
# Test unauthenticated access (expect 401)
curl -s -o /dev/null -w "%{http_code}" http://localhost/private/
# Test with correct credentials (expect 200)
curl -s --digest --user admin:secret123 -o /dev/null -w "%{http_code}" http://localhost/private/
# Test with wrong password (expect 401)
curl -s --digest --user admin:wrongpass -o /dev/null -w "%{http_code}" http://localhost/private/
Verify Brute-Force Protection
You can verify that the lockout mechanism works by sending multiple failed requests:
for i in $(seq 1 6); do
code=$(curl -s --digest --user admin:wrong -o /dev/null -w "%{http_code}" http://localhost/private/)
echo "Attempt $i: HTTP $code"
done
After the 5th failed attempt (with default auth_digest_maxtries 5), the server blocks the IP address. Check the error log for confirmation:
sudo tail /var/log/nginx/error.log
You will see:
ignoring authentication request - in evasion period, client: 127.0.0.1
Important: The lockout applies to the client IP address. A restart of NGINX clears the shared memory and resets all lockouts.
Security Best Practices
Always Use HTTPS
While digest auth protects the password from passive sniffing, it does not encrypt the rest of the communication. Always deploy NGINX digest authentication behind TLS:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
location /private/ {
auth_digest 'private';
auth_digest_user_file /etc/nginx/.digest_pw;
}
}
Combine with IP Restrictions
For sensitive areas, add an additional layer with IP-based access control:
location /admin/ {
allow 10.0.0.0/8;
deny all;
auth_digest 'admin';
auth_digest_user_file /etc/nginx/.digest_admin;
auth_digest_maxtries 3;
auth_digest_evasion_time 600s;
}
This approach requires both a trusted IP address and valid credentials to access the admin area.
Protect the Password File
Store password files outside the web root and restrict permissions:
sudo chmod 640 /etc/nginx/.digest_pw
sudo chown root:nginx /etc/nginx/.digest_pw
Use Strong Passwords
Since digest auth uses MD5 hashing, which is computationally inexpensive, passwords should be long and complex. Use at least 16 characters with a mix of letters, numbers, and symbols.
Tighten Brute-Force Limits
For high-security environments, reduce the allowed failure threshold and increase the lockout duration:
auth_digest_maxtries 3;
auth_digest_evasion_time 600s;
This limits attackers to just 3 attempts before being locked out for 10 minutes.
Troubleshooting
Authentication Always Fails
Realm mismatch: The most common cause. The realm in auth_digest must match the realm in your password file exactly (case-sensitive).
# Config uses this realm:
auth_digest 'private';
# Password file must use the same realm:
# admin:private:7552e1614f8d342d2c10d9369e0cf530
If you used htdigest -c /etc/nginx/.digest_pw Private admin (capital P), but your config says auth_digest 'private' (lowercase), authentication will always fail.
503 Service Unavailable
This means the shared memory zone is exhausted. Increase it:
auth_digest_shm_size 8m;
Check the error log for confirmation:
auth_digest ran out of shm space. Increase the auth_digest_shm_size limit.
Locked Out During Testing
If you triggered the brute-force protection during testing, restart NGINX to clear the shared memory:
sudo systemctl restart nginx
A reload (systemctl reload) does not clear shared memory. You must do a full restart.
Browser Keeps Asking for Credentials
If the browser prompts repeatedly after entering correct credentials, check:
auth_digest_expirestoo short: If set below 5 seconds, the nonce may expire before the browser can make subsequent requests for page resources (CSS, JS, images)auth_digest_replaystoo low: A single page load may need multiple authenticated requests. Ensure replays is at least 10
$remote_user is Empty
The digest authentication module does not set the $remote_user variable. This is a known limitation compared to basic authentication. If you need the authenticated username for logging or proxying, consider using basic auth over HTTPS instead.
Comparison with Other Authentication Methods
NGINX offers several authentication modules. Here is how NGINX digest authentication compares:
| Method | Password Protection | Brute-Force Built-in | Complexity | Best For |
|---|---|---|---|---|
| Basic Auth | None (Base64) | No | Simple | HTTPS-only environments |
| Digest Auth | MD5 challenge-response | Yes | Moderate | Internal networks, legacy systems |
| JWT Auth | Token-based | No | Complex | APIs, microservices |
| TOTP 2FA | Time-based OTP | No | Complex | High-security admin panels |
| LDAP Auth | Centralized | No | Complex | Enterprise with Active Directory |
Choose NGINX digest authentication when you need password-based access control with built-in brute-force protection and do not want to rely on TLS alone for credential security.
Conclusion
NGINX digest authentication provides a meaningful security upgrade over basic auth for environments where password confidentiality matters. The built-in brute-force protection with IP-based lockout adds a layer of defense that basic auth simply does not offer.
The module is straightforward to install from the GetPageSpeed repository and requires minimal configuration. For most deployments, the default settings work out of the box.
However, remember that digest auth is not a replacement for TLS. For the strongest protection, deploy it behind HTTPS and combine it with IP-based restrictions. For modern API authentication, consider JWT tokens or TOTP-based two-factor authentication instead.
The module source code is available on GitHub.
