yum upgrades for production use, this is the repository for you.
Active subscription is required.
You want to protect your downloadable content from unauthorized access. Perhaps you sell digital products and need time-limited download links. Maybe you run a media platform and want to prevent hotlinking. NGINX secure link functionality solves these problems effectively.
However, the built-in secure_link module has a critical weakness. It uses simple MD5 hashing rather than proper HMAC construction. This article shows you how to implement secure signed URLs using the HMAC Secure Link module instead.
What Are Signed URLs in NGINX?
Signed URLs are specially crafted links that contain a cryptographic token. This token proves the URL was generated by your server. When a user requests the URL, NGINX validates the token before serving the content.
The token typically includes several components. First, it contains a hash of the protected resource path. Second, it may include a timestamp for expiration checking. Third, the hash uses a secret key that only your server knows.
This approach offers several benefits. Users cannot guess or forge valid URLs. Links can expire after a set time. You can tie links to specific files, preventing URL tampering.
Why the Standard NGINX Secure Link Module Falls Short
NGINX ships with a built-in ngx_http_secure_link_module. Many administrators use it without understanding its cryptographic limitations.
Looking at the NGINX source code, the module computes hashes like this:
ngx_md5_init(&md5);
ngx_md5_update(&md5, val.data, val.len);
ngx_md5_final(md5_buf, &md5);
This is simple MD5 concatenation, not HMAC. The difference matters for security.
Understanding the HMAC Difference
HMAC (Hash-based Message Authentication Code) follows RFC 2104. It computes the hash as:
HMAC(K, m) = H((K XOR opad) || H((K XOR ipad) || m))
This construction provides several security properties that simple hash concatenation lacks. It prevents length extension attacks. It provides better resistance against collision attacks. It follows established cryptographic best practices.
The standard secure link module essentially computes MD5(message + secret). An attacker who discovers a hash collision could potentially forge tokens. With proper HMAC, this attack becomes computationally infeasible.
MD5 Is Cryptographically Broken
Beyond the HMAC issue, MD5 itself has known vulnerabilities. Researchers have demonstrated practical collision attacks against MD5 since 2004. While these attacks may not directly compromise your secure links today, using deprecated cryptography is poor security practice.
The HMAC Secure Link module addresses both problems. It implements proper HMAC construction with modern hash algorithms like SHA-256.
Installing the HMAC Secure Link Module
The HMAC Secure Link module is available as a pre-built package for RHEL-based distributions. This includes Rocky Linux, AlmaLinux, CentOS, and Red Hat Enterprise Linux versions 7 through 10.
Installation on Rocky Linux, AlmaLinux, and RHEL
First, enable the GetPageSpeed repository:
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
Then install the module:
dnf -y install nginx-module-hmac-secure-link
After installation, enable the module in /etc/nginx/nginx.conf. Add this line at the very top of the file, before any other directives:
load_module modules/ngx_http_hmac_secure_link_module.so;
Verify the installation by testing the NGINX configuration:
nginx -t
You should see output confirming the syntax is correct.
HMAC Secure Link Configuration Directives
The module provides four main directives for configuration. Understanding each directive helps you implement secure links correctly.
secure_link_hmac
This directive specifies where to find the token parameters in the request. It accepts a comma-separated list of variables.
Syntax: secure_link_hmac $arg_token,$arg_ts,$arg_e;
The parameters are:
– First value: The HMAC token from the URL
– Second value: The timestamp when the link was created
– Third value: The expiration period in seconds (optional)
secure_link_hmac_secret
This directive sets the secret key for HMAC computation. Choose a long, random string that only your server knows.
Syntax: secure_link_hmac_secret "your_secret_key_here";
Use a cryptographically random key of at least 32 characters. You can generate one using:
openssl rand -base64 32
secure_link_hmac_message
This directive defines what data gets signed. Include the URI and any parameters you want to protect.
Syntax: secure_link_hmac_message "$uri$arg_ts$arg_e";
By including the URI in the message, you prevent users from using a valid token for different files.
secure_link_hmac_algorithm
This directive selects the hash algorithm. The module supports all algorithms available in OpenSSL.
Syntax: secure_link_hmac_algorithm sha256;
Recommended algorithms include sha256, sha384, and sha512. Avoid md5 and sha1 due to known weaknesses.
Basic Secure Link Configuration
Here is a complete configuration for protecting a downloads directory:
server {
listen 80;
server_name downloads.example.com;
root /var/www/downloads;
location /files/ {
# Parse URL parameters
secure_link_hmac $arg_st,$arg_ts,$arg_e;
secure_link_hmac_secret "K7xB9pQ2mN4vR8sT1wY3zA5cD6fG0hJ2";
secure_link_hmac_message "$uri$arg_ts$arg_e";
secure_link_hmac_algorithm sha256;
# Empty string means invalid or missing token
if ($secure_link_hmac = "") {
return 403;
}
# Zero means the link has expired
if ($secure_link_hmac = "0") {
return 410;
}
# Token is valid, serve the file
}
}
The $secure_link_hmac variable returns three possible values:
– Empty string: Token is invalid or missing
– “0”: Token is valid but expired
– “1”: Token is valid and not expired
Test your configuration before reloading NGINX:
nginx -t && systemctl reload nginx
While using if directives in NGINX requires caution (see our guide on NGINX if is evil), they are safe here because we use only the return directive inside the if block.
Generating Secure Link Tokens
Your application must generate valid tokens for users to access protected content. The token generation process involves several steps:
- Construct the message string (URI + timestamp + expiration)
- Compute the HMAC-SHA256 hash using your secret key
- Encode the result in URL-safe Base64
- Build the complete URL with all parameters
Token Generation in PHP
PHP provides built-in functions for HMAC computation. Here is a complete example:
<?php
function generateSecureLink($uri, $secret, $expireSeconds = 3600) {
$timestamp = time();
$expire = $expireSeconds;
// Build the message exactly as NGINX expects
$message = $uri . $timestamp . $expire;
// Compute HMAC-SHA256 and encode as URL-safe Base64
$token = base64_encode(hash_hmac('sha256', $message, $secret, true));
$token = strtr($token, '+/', '-_');
$token = rtrim($token, '=');
// Build the complete URL
return $uri . '?st=' . $token . '&ts=' . $timestamp . '&e=' . $expire;
}
// Usage example
$secret = 'K7xB9pQ2mN4vR8sT1wY3zA5cD6fG0hJ2';
$downloadUrl = generateSecureLink('/files/document.pdf', $secret, 3600);
echo $downloadUrl;
This function creates links valid for one hour by default. Users receive a complete URL they can use immediately.
Token Generation in Python
Python’s hmac module makes token generation straightforward:
import hmac
import hashlib
import base64
import time
def generate_secure_link(uri, secret, expire_seconds=3600):
timestamp = int(time.time())
expire = expire_seconds
# Build the message
message = f"{uri}{timestamp}{expire}"
# Compute HMAC-SHA256
signature = hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).digest()
# URL-safe Base64 encoding
token = base64.b64encode(signature).decode()
token = token.replace('+', '-').replace('/', '_').rstrip('=')
return f"{uri}?st={token}&ts={timestamp}&e={expire}"
# Usage example
secret = 'K7xB9pQ2mN4vR8sT1wY3zA5cD6fG0hJ2'
url = generate_secure_link('/files/document.pdf', secret)
print(url)
Token Generation in Bash
For server-side scripts or testing, generate tokens using OpenSSL:
#!/bin/bash
SECRET="K7xB9pQ2mN4vR8sT1wY3zA5cD6fG0hJ2"
URI="/files/document.pdf"
TIMESTAMP=$(date +%s)
EXPIRE=3600
# Build the message
MESSAGE="${URI}${TIMESTAMP}${EXPIRE}"
# Generate HMAC-SHA256 token with URL-safe Base64
TOKEN=$(echo -n "$MESSAGE" | \
openssl dgst -sha256 -hmac "$SECRET" -binary | \
base64 | \
tr '+/' '-_' | \
tr -d '=')
echo "URL: ${URI}?st=${TOKEN}&ts=${TIMESTAMP}&e=${EXPIRE}"
Make the script executable and run it to generate download links.
Implementing Hotlink Protection
Hotlink protection prevents other websites from embedding your images or media directly. Without protection, external sites consume your bandwidth while serving content to their visitors.
Traditional hotlink protection uses the Referer header. However, users can easily forge this header. NGINX secure link provides stronger protection because tokens cannot be forged without the secret key.
Configuration for Image Hotlink Protection
location ~* ^/images/.*\.(jpg|jpeg|png|gif|webp)$ {
secure_link_hmac $arg_token,$arg_ts,$arg_e;
secure_link_hmac_secret "image_protection_secret_key";
secure_link_hmac_message "$uri$arg_ts$arg_e";
secure_link_hmac_algorithm sha256;
if ($secure_link_hmac != "1") {
# Optionally redirect to a placeholder image
rewrite ^ /images/placeholder.png last;
}
}
location = /images/placeholder.png {
# Serve placeholder without authentication
}
With this configuration, images require valid tokens. Hotlinkers see only your placeholder image.
Generating Image URLs in Your Application
When outputting images in your HTML, generate signed URLs dynamically:
<img src="<?php echo generateSecureLink('/images/photo.jpg', $secret, 86400); ?>" alt="Protected image">
This creates image URLs valid for 24 hours. Adjust the expiration based on your caching strategy.
Advanced Use Cases
The HMAC Secure Link module supports several advanced scenarios beyond basic file protection.
Video Streaming Protection
Protect video content by requiring valid tokens for each request:
location ~* ^/videos/.*\.(mp4|webm|m3u8|ts)$ {
secure_link_hmac $arg_st,$arg_ts,$arg_e;
secure_link_hmac_secret "video_streaming_secret";
secure_link_hmac_message "$uri$arg_ts$arg_e";
secure_link_hmac_algorithm sha256;
if ($secure_link_hmac = "") {
return 403;
}
if ($secure_link_hmac = "0") {
return 410;
}
# Enable byte-range requests for video seeking
add_header Accept-Ranges bytes;
}
For HLS streaming, sign each segment URL individually. Your video player must handle token generation for segment requests.
Proxy Token Generation
In proxy scenarios, NGINX can generate tokens for upstream servers. This enables authentication chains across multiple servers.
location /proxy/ {
secure_link_hmac_message "$uri$time_iso8601$expire";
secure_link_hmac_secret "upstream_secret_key";
secure_link_hmac_algorithm sha256;
proxy_pass http://backend$uri?token=$secure_link_hmac_token&ts=$time_iso8601;
}
The $secure_link_hmac_token variable contains the generated token for the upstream request.
IP-Based Token Binding
Bind tokens to specific IP addresses for additional security:
location /restricted/ {
secure_link_hmac $arg_st,$arg_ts,$arg_e;
secure_link_hmac_secret "ip_bound_secret";
secure_link_hmac_message "$uri$arg_ts$arg_e$remote_addr";
secure_link_hmac_algorithm sha256;
if ($secure_link_hmac != "1") {
return 403;
}
}
Update your token generation to include the user’s IP address:
$message = $uri . $timestamp . $expire . $_SERVER['REMOTE_ADDR'];
Tokens generated for one IP address fail validation when used from another IP.
Combining with Other Access Control Methods
NGINX secure link works alongside other access control mechanisms. For defense in depth, combine multiple protection layers.
With Basic Authentication
Require both a valid token and username/password for sensitive content. See our NGINX basic auth guide for password file setup:
location /admin-files/ {
secure_link_hmac $arg_st,$arg_ts,$arg_e;
secure_link_hmac_secret "admin_files_secret";
secure_link_hmac_message "$uri$arg_ts$arg_e";
secure_link_hmac_algorithm sha256;
if ($secure_link_hmac != "1") {
return 403;
}
auth_basic "Restricted Files";
auth_basic_user_file /etc/nginx/htpasswd;
}
With IP Allowlisting
Restrict access to known IP addresses in addition to token validation. Our NGINX allow deny guide covers IP-based access control:
location /internal/ {
secure_link_hmac $arg_st,$arg_ts,$arg_e;
secure_link_hmac_secret "internal_secret";
secure_link_hmac_message "$uri$arg_ts$arg_e";
secure_link_hmac_algorithm sha256;
if ($secure_link_hmac != "1") {
return 403;
}
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
}
Troubleshooting Common Issues
Several issues commonly arise when implementing NGINX secure links. This section helps you diagnose and fix them.
All Requests Return 403
If every request fails authentication, check these items:
- Verify the secret key matches between your application and NGINX
- Ensure the message string construction is identical on both sides
- Check that Base64 encoding uses URL-safe characters
- Confirm the module is loaded in
nginx.conf
Enable debug logging temporarily:
error_log /var/log/nginx/error.log debug;
The debug log shows the computed and expected hash values.
Links Expire Too Quickly
The expiration check uses the timestamp parameter plus the expiration period. Ensure your server clock is synchronized using NTP:
timedatectl status
If the clock is off, tokens may appear expired immediately.
Token Contains Invalid Characters
URL-safe Base64 encoding replaces certain characters:
– Plus (+) becomes hyphen (-)
– Slash (/) becomes underscore (_)
– Padding (=) is removed
Ensure your application performs all three transformations.
Security Best Practices
Follow these practices to maximize the security of your implementation.
Use Strong Secret Keys
Generate keys using cryptographically secure random number generators:
openssl rand -base64 48
Never use predictable values like passwords or dates.
Rotate Keys Periodically
Change your secret keys regularly, perhaps monthly or quarterly. Plan the rotation to avoid invalidating active links:
- Add the new key alongside the old one
- Generate new links with the new key
- Wait for old links to expire
- Remove the old key
Set Appropriate Expiration Times
Shorter expiration times reduce the window for token abuse. Consider these guidelines:
- Download links: 1-24 hours
- Embedded images: 24-72 hours
- Streaming segments: 5-15 minutes
Balance security against user experience. Links that expire during legitimate use frustrate users.
Monitor for Abuse
Log failed authentication attempts:
location /files/ {
secure_link_hmac $arg_st,$arg_ts,$arg_e;
secure_link_hmac_secret "your_secret";
secure_link_hmac_message "$uri$arg_ts$arg_e";
secure_link_hmac_algorithm sha256;
if ($secure_link_hmac = "") {
access_log /var/log/nginx/secure_link_failures.log;
return 403;
}
}
Review these logs for patterns indicating attack attempts.
Validate Your Configuration
Use Gixy to check your NGINX configuration for security issues:
gixy /etc/nginx/nginx.conf
Gixy identifies common misconfigurations that could compromise security.
Performance Considerations
HMAC computation adds minimal overhead to each request. SHA-256 processes data extremely fast on modern CPUs. You can expect microsecond-level latency impact per request.
For extremely high-traffic scenarios, consider caching authentication results:
location /files/ {
secure_link_hmac $arg_st,$arg_ts,$arg_e;
secure_link_hmac_secret "your_secret";
secure_link_hmac_message "$uri$arg_ts$arg_e";
secure_link_hmac_algorithm sha256;
if ($secure_link_hmac != "1") {
return 403;
}
# Enable caching for authenticated requests
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
}
The file cache reduces disk I/O for frequently accessed files.
Comparing Secure Link to Other Solutions
NGINX secure link competes with several alternative approaches for content protection.
Pre-Signed URLs (S3-Style)
Cloud storage services like S3 use similar signed URL mechanisms. NGINX secure link provides equivalent functionality for self-hosted content without cloud vendor lock-in.
Token Authentication Services
Dedicated services like OAuth or JWT provide comprehensive authentication. However, they add complexity and latency. NGINX secure link offers lighter-weight protection directly at the web server layer.
DRM Solutions
Digital Rights Management systems provide stronger protection for premium content. They prevent screen recording and redistribution. For most use cases, signed URLs provide sufficient protection with far less complexity.
Conclusion
NGINX secure link functionality protects your content from unauthorized access effectively. The HMAC Secure Link module improves on the standard implementation with proper cryptographic construction.
Key takeaways from this guide:
- Use the HMAC module instead of the standard secure link module
- Choose SHA-256 or stronger for the hash algorithm
- Generate strong, random secret keys
- Set appropriate expiration times for your use case
- Monitor authentication failures for abuse detection
Implement these practices to protect your downloadable content, prevent hotlinking, and control access to your media files.
