Site icon GetPageSpeed

NGINX Auth Hash Module: Secure URL Authentication

NGINX Auth Hash Module: Secure URL Authentication with SHA-256 and Beyond

The NGINX auth hash module brings modern cryptographic hash algorithms to URL signing and content protection. If you need to protect file downloads, generate expiring links, or prevent unauthorized access to resources served by NGINX, the auth hash module provides a flexible and secure solution.

NGINX’s built-in secure_link module has been the go-to approach for URL signing for years. However, it only supports MD5 hashing. The NGINX auth hash module fixes this by supporting any algorithm that OpenSSL provides — SHA-256, SHA-512, SHA-3, BLAKE2, and more. It also offers multiple token encoding formats, flexible timestamp handling, and timezone support.

NGINX ships with ngx_http_secure_link_module, which provides two modes of URL signing. Both are hardcoded to MD5:

# Native secure_link - MD5 only, cannot be changed
secure_link $arg_md5,$arg_expires;
secure_link_md5 "$secure_link_expires$uri$remote_addr my_secret";

While MD5 is computationally fast, it presents several problems:

The auth hash module addresses all of these shortcomings while maintaining the same configuration simplicity that NGINX administrators expect.

How the Auth Hash Module Works

The NGINX auth hash module operates on a straightforward principle:

  1. Your application generates a URL with a cryptographic hash token and (optionally) a timestamp.
  2. When NGINX receives the request, the module reconstructs the expected hash from the configured message template and secret key.
  3. If the provided token matches the computed hash, and the timestamp falls within the valid time range, the $auth_hash variable is set to "1".
  4. Your NGINX configuration checks $auth_hash and either serves the content or returns a 403 error.

The hash comparison uses OpenSSL’s CRYPTO_memcmp() function, which performs constant-time comparison to prevent timing attacks.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the NGINX auth hash module from the GetPageSpeed RPM repository:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-auth-hash

Then load the module by adding this line at the top of /etc/nginx/nginx.conf:

load_module modules/ngx_http_auth_hash_module.so;

You can find more NGINX modules at the GetPageSpeed NGINX Extras collection, which includes modules for security hardening, performance optimization, and advanced request processing.

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-auth-hash

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

Verify the module is loaded by running nginx -t:

nginx -t

If you see no errors about unknown directives, the module is ready.

Configuration Reference

The NGINX auth hash module provides six directives and two variables. All directives are valid in http, server, and location contexts.

auth_hash

Enables or disables URL hash authentication for the current context.

Property Value
Syntax auth_hash on \| off;
Default off
Context http, server, location

When disabled, the $auth_hash variable is always empty (not found).

auth_hash_algorithm

Specifies the cryptographic hash function to use. Any algorithm name recognized by OpenSSL’s EVP_get_digestbyname() function is valid.

Property Value
Syntax auth_hash_algorithm <name>;
Default sha256
Context http, server, location

Common algorithm choices:

Algorithm Hash Length Notes
sha256 32 bytes (64 hex chars) Recommended default. Fast and secure.
sha512 64 bytes (128 hex chars) Stronger but longer tokens.
sha3-256 32 bytes (64 hex chars) Latest SHA standard.
blake2b512 64 bytes (128 hex chars) Very fast on modern CPUs.
sha384 48 bytes (96 hex chars) Common in TLS cipher suites.

To list all available algorithms on your system, run:

openssl list -digest-algorithms

auth_hash_secret

Defines the secret key used for hash computation. This value supports NGINX variables, which means you can load it from headers, cookies, or other dynamic sources.

Property Value
Syntax auth_hash_secret <value>;
Default
Context http, server, location

Security tip: Store your secret in a separate file and include it:

# /etc/nginx/auth_hash_secret.conf (chmod 600)
auth_hash_secret "a-long-random-secret-key-here";
location /protected/ {
    include /etc/nginx/auth_hash_secret.conf;
    # ...
}

This keeps the secret out of your main configuration file, making it easier to manage access controls and rotate keys.

auth_hash_message

Defines the message string that gets hashed. This is the data that both your application and NGINX must agree on. It supports NGINX variables.

Property Value
Syntax auth_hash_message <value>;
Default
Context http, server, location

A typical message includes the URI, timestamp, expiration window, and secret:

auth_hash_message "$uri|$arg_ts|$arg_e|$auth_hash_secret";

The pipe character | is a common delimiter, but you can use any separator. The message must be constructed identically on both the application side and the NGINX side.

auth_hash_check_token

Specifies where to find the hash token in the request and what encoding format it uses.

Property Value
Syntax auth_hash_check_token <value> [digest=hex\|base64\|base64url\|bin];
Default digest is hex
Context http, server, location

Supported digest formats:

Format Description Example
hex Hexadecimal string (default) a1b2c3d4e5...
base64url URL-safe Base64 obLD1OX...
base64 Standard Base64 obLD1OX+/...
bin Raw binary (not URL-safe)

For URLs, hex or base64url are the practical choices. The hex format produces longer tokens but is safe in all URL contexts without encoding. The base64url format produces shorter tokens that are also URL-safe.

auth_hash_check_time

Configures time-based validation for link expiration. This directive is optional. If omitted, links never expire.

Property Value
Syntax auth_hash_check_time <value> [format=<fmt>] [timezone=<tz>] [range_start=<n>] [range_end=<n>];
Default
Context http, server, location

Time format options:

Format Description Example Value
%s Unix timestamp in seconds (default) 1772120011
%ms Unix timestamp in milliseconds 1772120011234
%x Hexadecimal Unix timestamp 69a067d4
Custom Any strptime()-compatible format %Y-%m-%dT%H:%M:%S

Timezone: Specify as gmt, gmt+0800, gmt-0500, etc. Only applies when using a custom date format.

Range parameters: Control when a link becomes valid and when it expires:

For example, range_start=0 range_end=$arg_e means the link is valid from the moment of the timestamp until $arg_e seconds later.

Embedded Variables

$auth_hash

Returns "1" if the hash is correct and the link has not expired. Returns an empty value (not found) otherwise. Use this variable in an if block to control access:

if ($auth_hash != "1") {
    return 403;
}

$auth_hash_secret

Contains the current value of the auth_hash_secret directive. This is useful when constructing the auth_hash_message string, because it allows you to reference the secret within the message template without repeating the literal value:

auth_hash_secret "my_secret_key";
auth_hash_message "$uri|$arg_ts|$arg_e|$auth_hash_secret";

Practical Configuration Examples

This is the most common use case: your application generates time-limited download URLs, and NGINX validates the auth hash before serving the file.

NGINX configuration:

location /downloads/ {
    auth_hash on;
    auth_hash_algorithm sha256;
    auth_hash_secret "change-this-to-a-long-random-string";
    auth_hash_check_time $arg_ts range_end=$arg_e format=%s;
    auth_hash_check_token $arg_st;
    auth_hash_message "$uri|$arg_ts|$arg_e|$auth_hash_secret";

    if ($auth_hash != "1") {
        return 403;
    }

    alias /var/www/protected-files/;
}

Generate download links in PHP:

$secret = 'change-this-to-a-long-random-string';
$uri = '/downloads/report.pdf';
$timestamp = time();
$expire = 3600; // link valid for 1 hour

$message = "{$uri}|{$timestamp}|{$expire}|{$secret}";
$token = hash('sha256', $message);

$download_url = "https://example.com{$uri}?st={$token}&ts={$timestamp}&e={$expire}";

Generate download links in Python:

import hashlib
import time

secret = 'change-this-to-a-long-random-string'
uri = '/downloads/report.pdf'
timestamp = int(time.time())
expire = 3600

message = f"{uri}|{timestamp}|{expire}|{secret}"
token = hashlib.sha256(message.encode()).hexdigest()

download_url = f"https://example.com{uri}?st={token}&ts={timestamp}&e={expire}"

Generate download links in Node.js:

const crypto = require('crypto');

const secret = 'change-this-to-a-long-random-string';
const uri = '/downloads/report.pdf';
const timestamp = Math.floor(Date.now() / 1000);
const expire = 3600;

const message = `${uri}|${timestamp}|${expire}|${secret}`;
const token = crypto.createHash('sha256').update(message).digest('hex');

const downloadUrl = `https://example.com${uri}?st=${token}&ts=${timestamp}&e=${expire}`;

Generate download links in Bash:

SECRET="change-this-to-a-long-random-string"
URI="/downloads/report.pdf"
TS=$(date +%s)
EXPIRE=3600

MESSAGE="${URI}|${TS}|${EXPIRE}|${SECRET}"
TOKEN=$(echo -n "$MESSAGE" | openssl dgst -sha256 | awk '{print $NF}')

echo "https://example.com${URI}?st=${TOKEN}&ts=${TS}&e=${EXPIRE}"

Permanent Signed URLs (No Expiration)

For content that should always be accessible to anyone with the correct link, omit auth_hash_check_time entirely:

location /shared/ {
    auth_hash on;
    auth_hash_algorithm sha256;
    auth_hash_secret "shared-content-secret";
    auth_hash_check_token $arg_token;
    auth_hash_message "$uri|$auth_hash_secret";

    if ($auth_hash != "1") {
        return 403;
    }

    alias /var/www/shared-files/;
}

This is useful for shareable links that never expire — similar to how cloud storage share links work. The URL itself serves as the access credential.

Using Base64URL for Shorter Tokens

SHA-256 hex tokens are 64 characters long. If you prefer shorter URLs, use base64url encoding, which produces 43-character tokens:

location /compact/ {
    auth_hash on;
    auth_hash_algorithm sha256;
    auth_hash_secret "compact-secret";
    auth_hash_check_token $arg_t digest=base64url;
    auth_hash_check_time $arg_ts range_end=$arg_e format=%s;
    auth_hash_message "$uri|$arg_ts|$arg_e|$auth_hash_secret";

    if ($auth_hash != "1") {
        return 403;
    }

    try_files $uri =404;
}

Generate the base64url token in PHP:

$message = "{$uri}|{$timestamp}|{$expire}|{$secret}";
$hash_binary = hash('sha256', $message, true); // raw binary output
$token = rtrim(strtr(base64_encode($hash_binary), '+/', '-_'), '=');

Using SHA-512 for Maximum Security

For environments that require the strongest available hash, use SHA-512:

location /vault/ {
    auth_hash on;
    auth_hash_algorithm sha512;
    auth_hash_secret "vault-secret-key";
    auth_hash_check_token $arg_token;
    auth_hash_check_time $arg_ts range_end=$arg_e format=%s;
    auth_hash_message "$uri|$arg_ts|$arg_e|$auth_hash_secret";

    if ($auth_hash != "1") {
        return 403;
    }

    alias /var/www/vault/;
}

Using Date Strings with Timezone

If your application generates human-readable timestamps instead of Unix epoch values, configure the NGINX auth hash module with a custom date format:

location /reports/ {
    auth_hash on;
    auth_hash_algorithm sha256;
    auth_hash_secret "report-secret";
    auth_hash_check_time $arg_ts range_end=$arg_e format=%Y-%m-%dT%H:%M:%S timezone=gmt+0000;
    auth_hash_check_token $arg_st;
    auth_hash_message "$uri|$arg_ts|$arg_e|$auth_hash_secret";

    if ($auth_hash != "1") {
        return 403;
    }

    alias /var/www/reports/;
}

The resulting URL looks like:

https://example.com/reports/q4.pdf?st=abc123...&ts=2026-02-26T15:30:00&e=3600

The timezone parameter adjusts for the timezone offset in the date string. Use gmt+0000 for UTC, gmt+0900 for JST, gmt-0500 for EST, and so on.

Testing Your Configuration

After setting up the NGINX auth hash module, verify it works correctly.

Step 1: Test that unsigned requests are blocked.

curl -s -o /dev/null -w "%{http_code}" http://localhost/downloads/report.pdf
# Expected: 403

Step 2: Test that invalid tokens are rejected.

curl -s -o /dev/null -w "%{http_code}" \
  "http://localhost/downloads/report.pdf?st=invalidtoken&ts=$(date +%s)&e=3600"
# Expected: 403

Step 3: Generate a valid token and test.

URI="/downloads/report.pdf"
TS=$(date +%s)
E=3600
SECRET="change-this-to-a-long-random-string"
MSG="${URI}|${TS}|${E}|${SECRET}"
TOKEN=$(echo -n "$MSG" | openssl dgst -sha256 | awk '{print $NF}')

curl -s -o /dev/null -w "%{http_code}" \
  "http://localhost${URI}?st=${TOKEN}&ts=${TS}&e=${E}"
# Expected: 200

Step 4: Test that expired tokens are rejected.

PAST_TS=$(($(date +%s) - 7200))  # 2 hours ago
E=60  # was valid for only 60 seconds
MSG="${URI}|${PAST_TS}|${E}|${SECRET}"
TOKEN=$(echo -n "$MSG" | openssl dgst -sha256 | awk '{print $NF}')

curl -s -o /dev/null -w "%{http_code}" \
  "http://localhost${URI}?st=${TOKEN}&ts=${PAST_TS}&e=${E}"
# Expected: 403

Performance Considerations

The NGINX auth hash module adds minimal overhead to request processing. The hash computation happens once per request and takes microseconds even with SHA-512. Here are specific points to consider:

The module does not add any I/O overhead — it only performs CPU-bound hash computation on data already available in the request. Therefore, under high concurrency, the NGINX auth hash module will not become a bottleneck.

Security Best Practices

Use a Strong Secret Key

Generate a random secret of at least 32 characters:

openssl rand -base64 32

Always Set an Expiration Window

Links without expiration remain valid forever. If a signed URL leaks, anyone can use it indefinitely. Always set range_end to the shortest practical duration:

auth_hash_check_time $arg_ts range_end=$arg_e format=%s;

Then in your application, set e (expiration) to the minimum needed — 300 seconds (5 minutes) for immediate downloads, or 3600 seconds (1 hour) for email links.

Include the URI in the Hash Message

Always include $uri in auth_hash_message. Without it, a token generated for one file could be reused to access a different file:

# GOOD: token is bound to the specific URI
auth_hash_message "$uri|$arg_ts|$arg_e|$auth_hash_secret";

# BAD: token works for any file in this location
auth_hash_message "$arg_ts|$arg_e|$auth_hash_secret";

Bind Tokens to Client IP (Optional)

For maximum security, include the client’s IP address in the hash message. This prevents token sharing:

auth_hash_message "$uri|$remote_addr|$arg_ts|$arg_e|$auth_hash_secret";

Your application must then include the user’s IP when generating the token. Be aware that this breaks the link for users behind changing IP addresses (mobile networks, some VPNs).

Do Not Reveal Failure Reasons

Always return a generic 403 error. Never expose whether the hash was wrong, the link expired, or the token format was invalid:

if ($auth_hash != "1") {
    return 403;
}

The module logs detailed error reasons (token mismatch, expiration, decode failure) at the error log level for server-side debugging, but the client only sees 403 Forbidden.

Rotate Secrets Periodically

When you rotate your secret key, existing signed URLs will stop working. Plan for this:

  1. Generate a new secret.
  2. Update your application to sign URLs with the new secret.
  3. Wait for existing links to expire (based on your range_end values).
  4. Update the NGINX configuration with the new secret.
  5. Reload NGINX with nginx -s reload.

Troubleshooting

“unknown directive auth_hash”

The module is not loaded. Verify that load_module modules/ngx_http_auth_hash_module.so; is at the top of nginx.conf, and that the module file exists:

ls /usr/lib64/nginx/modules/ngx_http_auth_hash_module.so

Valid Tokens Return 403

Check the NGINX error log for detailed messages:

tail -f /var/log/nginx/error.log

Common causes:

Clock Skew Between Servers

If your application server and NGINX server have different system times, time-based validation will fail. Use NTP to keep clocks synchronized:

sudo timedatectl set-ntp true

For environments where minor clock skew is unavoidable, add a small buffer to range_start:

auth_hash_check_time $arg_ts range_start=-30 range_end=$arg_e format=%s;

This allows links to be valid up to 30 seconds before the timestamp, accommodating clock differences.

Feature secure_link (built-in) auth hash module
Hash algorithm MD5 only Any OpenSSL algorithm
Token format base64url only hex, base64, base64url, binary
Time format Unix timestamp only Unix, milliseconds, hex, custom date
Timezone support No Yes
Time range (start/end) Expiration only Both activation and expiration
Timing-safe comparison No Yes (CRYPTO_memcmp)
Compliance-friendly No (MD5) Yes (SHA-256+)

If your environment has no compliance requirements and MD5 is acceptable, the built-in secure_link module works fine. For everything else, the NGINX auth hash module is the modern replacement.

Conclusion

The NGINX auth hash module provides a flexible, secure way to protect content with signed URLs. It overcomes the MD5-only limitation of the built-in secure_link module by supporting any hash algorithm that OpenSSL provides. Whether you are protecting file downloads, generating temporary access links, or hardening your content delivery pipeline, this module gives you the cryptographic flexibility that modern deployments require.

The module is available as a pre-built package for RHEL-based and Debian-based systems from the GetPageSpeed repository. The source code is 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