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.
Why Not Use the Built-in secure_link Module?
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:
- MD5 is cryptographically weak. Although collisions in HMAC-MD5 are not practically exploitable today, many compliance frameworks (PCI DSS, HIPAA, FedRAMP) explicitly prohibit MD5 in new deployments.
- No algorithm flexibility. You cannot switch to SHA-256 or another algorithm without replacing the module entirely.
- Limited token format. The native module only accepts base64url-encoded tokens. The NGINX auth hash module additionally supports hex, standard base64, and raw binary.
- No timezone support. The native module requires Unix timestamps with no timezone offset handling.
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:
- Your application generates a URL with a cryptographic hash token and (optionally) a timestamp.
- When NGINX receives the request, the module reconstructs the expected hash from the configured message template and secret key.
- If the provided token matches the computed hash, and the timestamp falls within the valid time range, the
$auth_hashvariable is set to"1". - Your NGINX configuration checks
$auth_hashand 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_moduledirective 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:
range_start=N— the link becomes validNseconds after the timestamp.range_end=N— the link expiresNseconds after the timestamp. This is typically passed as a URL parameter so each link can have its own expiration window.
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
Protecting File Downloads with Expiring Links
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:
- SHA-256 processes roughly 500 MB/s on modern hardware. A single hash of a typical 100-byte message takes under 1 microsecond.
- SHA-512 is actually faster than SHA-256 on 64-bit systems for longer messages, though both are negligible for URL signing.
- BLAKE2b512 is the fastest option, designed specifically for high-speed hashing.
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:
- Generate a new secret.
- Update your application to sign URLs with the new secret.
- Wait for existing links to expire (based on your
range_endvalues). - Update the NGINX configuration with the new secret.
- 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:
- “auth hash: token value mismatch” — the message string differs between your application and NGINX. Print both strings and compare them byte-by-byte.
- “auth hash: request not yet valid or expired” — the timestamp is outside the valid time range. Check clock synchronization between your application server and NGINX server.
- “auth hash: token hex decode fail” — the token is not valid hex. Ensure your application produces lowercase hex output.
- “auth hash: token len mismatch” — the token length does not match the expected hash output. Verify you are using the same algorithm on both sides.
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.
Comparison with the Native secure_link Module
| 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.

