Skip to main content

NGINX / Security

NGINX Secure Link: Signed URLs and Hotlink Protection

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth 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.

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.

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.

The module provides four main directives for configuration. Understanding each directive helps you implement secure links correctly.

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)

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

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.

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.

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.

Your application must generate valid tokens for users to access protected content. The token generation process involves several steps:

  1. Construct the message string (URI + timestamp + expiration)
  2. Compute the HMAC-SHA256 hash using your secret key
  3. Encode the result in URL-safe Base64
  4. 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.

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.

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:

  1. Verify the secret key matches between your application and NGINX
  2. Ensure the message string construction is identical on both sides
  3. Check that Base64 encoding uses URL-safe characters
  4. 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.

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:

  1. Add the new key alongside the old one
  2. Generate new links with the new key
  3. Wait for old links to expire
  4. 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.

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.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.