Site icon GetPageSpeed

NGINX Digest Authentication: More Secure Than Basic Auth

NGINX Digest Authentication: More Secure Than Basic Auth

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:

  1. Client sends a request without credentials
  2. Server responds with 401 Unauthorized and includes a WWW-Authenticate header containing a random nonce, the realm name, and the supported algorithm (MD5)
  3. Client computes a digest response by hashing the username, realm, password, nonce, request method, and URI together
  4. Client sends the request again with an Authorization header containing the computed digest
  5. Server verifies the digest by performing the same calculation using the stored password hash
  6. Server responds with 200 OK and includes an Authentication-Info header 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:

When to use basic auth instead:

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

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:

  1. auth_digest_expires too short: If set below 5 seconds, the nonce may expire before the browser can make subsequent requests for page resources (CSS, JS, images)
  2. auth_digest_replays too 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.

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