Site icon GetPageSpeed

NGINX Secure Token Module: CDN Token Authentication

NGINX Secure Token Module: Protect Streaming Content with CDN Token Authentication

The Problem: Unprotected Streaming URLs

HLS and DASH streaming architectures expose a fundamental security weakness that the NGINX secure token module is designed to solve. A video player receives a manifest file — an .m3u8 playlist or .mpd document — that lists every segment URL in plain text. Anyone who intercepts or inspects that manifest can download every segment directly, bypassing your application’s access controls entirely. Worse, those URLs can be shared, scraped by bots, or embedded on unauthorized sites.

Traditional solutions have significant gaps when applied to streaming:

The NGINX secure token module solves all three problems. It operates as a body filter that intercepts streaming manifests, generates CDN-compatible cryptographic tokens, and injects them into every segment URL — automatically, at the edge, with no application changes required. The module supports seven CDN token formats and additionally offers AES-256 URI encryption to fully obfuscate segment paths.

How the NGINX Secure Token Module Works

The NGINX secure token module operates as a body filter in the NGINX request processing pipeline. When NGINX serves a streaming manifest — an HLS .m3u8 playlist, a DASH .mpd manifest, or an HDS .f4m manifest — the module intercepts the response body and modifies every segment URL inside it by appending a cryptographic token.

Here is the flow in detail:

  1. Token definition: You define a named token block in the http context (for example, secure_token_akamai $my_token { ... }). This block specifies the signing key, access control list (ACL), and expiration time.
  2. Token binding: In the location block that serves your manifests, you bind the token variable with secure_token $my_token and specify which content types trigger tokenization via secure_token_types.
  3. Manifest rewriting: When a client requests a manifest, the module’s body filter parses the manifest format, locates every segment URL, and appends the generated token as a query string parameter.
  4. Non-manifest responses: For content types that are not manifests (such as .ts video segments), the token is delivered as an HTTP Set-Cookie header instead.

This design means the client receives a manifest where every segment URL already contains a valid, time-limited token. CDN edge servers then validate these tokens before serving each segment.

By default, the module prefers query string tokens for manifest types (secure_token_avoid_cookies is enabled). This means:

This behavior ensures maximum compatibility with CDN edge validation while keeping manifest URLs self-contained.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the module from the GetPageSpeed RPM repository:

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

Then load the module by adding this line at the very top of /etc/nginx/nginx.conf, before the events block:

load_module modules/ngx_http_secure_token_filter_module.so;

Verify the module loads correctly:

nginx -t

Debian and Ubuntu

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

sudo apt-get update
sudo apt-get install nginx-module-secure-token

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

You can find detailed package information on the APT module page.

Configuration

The NGINX secure token module configuration has two layers: CDN-specific token blocks defined in the http context, and per-location tokenization settings. Understanding this structure is essential for correct deployment.

Step 1: Define a Token Block

Token blocks must be placed in the http context (not inside server or location). Each block creates an NGINX variable that evaluates to a signed token string at request time.

Step 2: Bind the Token in a Location

Inside your location block, reference the token variable with secure_token and specify which MIME types should trigger tokenization with secure_token_types.

Akamai Token Configuration

Akamai EdgeAuth v2 is the most widely used token format for CDN content protection. The module generates HMAC-SHA256 signed tokens compatible with Akamai’s edge validation.

Basic Akamai Setup

# In http context: define the token
secure_token_akamai $akamai_token {
    key 000102030405060708090a0b0c0d0e0f1011121314151617;
    acl "$secure_token_baseuri_comma*";
    end 1h;
}

server {
    listen 443 ssl;
    server_name stream.example.com;

    location /video/ {
        root /srv/media;

        # Bind token and set types
        secure_token $akamai_token;
        secure_token_types application/vnd.apple.mpegurl;

        # Cache control
        secure_token_expires_time 100d;
        secure_token_query_token_expires_time 1h;
    }
}

With this configuration, an HLS manifest request to /video/playlist.m3u8 returns content like:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXTINF:10.0,
segment0.ts?__hdnea__=st=1774089648~exp=1774093248~acl=/video/*~hmac=85dda94c...
#EXTINF:10.0,
segment1.ts?__hdnea__=st=1774089648~exp=1774093248~acl=/video/*~hmac=85dda94c...

Every segment URL now includes a time-limited HMAC-SHA256 token. The acl parameter restricts the token to the /video/ path, and the end 1h setting makes the token expire one hour from the time of generation.

Akamai Token with IP Restriction

For maximum security, bind the token to the client’s IP address. This prevents token sharing or theft — even if someone intercepts a manifest URL, the token will not work from a different IP:

secure_token_akamai $akamai_token_ip {
    key 000102030405060708090a0b0c0d0e0f1011121314151617;
    acl "$secure_token_baseuri_comma*";
    ip_address $remote_addr;
    end 1h;
}

The generated token now includes an ip= field:

segment0.ts?__hdnea__=ip=203.0.113.42~st=...~exp=...~acl=/video/*~hmac=...

Akamai Block Parameters

Parameter Default Description
key (mandatory) Hex-encoded HMAC-SHA256 signing key
acl $secure_token_baseuri_comma Access control path pattern (supports variables)
param_name __hdnea__ Query parameter name for the token
start 0 (now) Token validity start time
end 86400 (1 day) Token expiration time
ip_address (none) Restrict token to a specific IP (supports variables like $remote_addr)

Amazon CloudFront Token Configuration

CloudFront tokens use RSA-signed policies, which is a different cryptographic approach from Akamai’s HMAC-SHA256. You need an RSA private key and a CloudFront key pair ID.

Important: SHA-1 Crypto Policy Requirement. CloudFront token signing uses SHA-1 internally. On RHEL 9+, Rocky Linux 9+, and AlmaLinux 9+, the DEFAULT system crypto policy disables SHA-1 for signatures. If CloudFront tokens silently fail (segments appear without tokens, no errors in logs), you must enable the LEGACY crypto policy:

sudo update-crypto-policies --set LEGACY
sudo systemctl restart nginx

This affects the entire system’s cryptographic standards. On RHEL 8 and older distributions, CloudFront tokens work without any policy changes.

CloudFront Setup

secure_token_cloudfront $cf_token {
    private_key_file /etc/nginx/keys/cloudfront-private.pem;
    key_pair_id APKAEXAMPLE123;
    acl "$scheme://$http_host$secure_token_baseuri_comma*";
    end 1h;
}

server {
    listen 443 ssl;
    server_name stream.example.com;

    location /video/ {
        root /srv/media;
        secure_token $cf_token;
        secure_token_types application/vnd.apple.mpegurl;
        secure_token_expires_time 100d;
        secure_token_query_token_expires_time 1h;
    }
}

CloudFront tokens contain three query parameters: Policy (base64-encoded JSON policy), Signature (RSA signature), and Key-Pair-Id.

CloudFront Block Parameters

Parameter Default Description
private_key_file (mandatory) Path to RSA private key PEM file
key_pair_id (mandatory) CloudFront key pair identifier
acl $secure_token_baseuri_comma Signed URL pattern (supports variables)
end 86400 (1 day) Policy expiration time
ip_address (none) Restrict token to IP (use $remote_addr/32 for single IP)

Other Supported CDN Formats

The NGINX secure token module supports five additional CDN token formats beyond Akamai and CloudFront:

Broadpeak

secure_token_broadpeak $bp_token {
    key "my_secret_key";
    acl "$secure_token_baseuri_comma*";
    param_name "token";
    end 1h;
}

Broadpeak tokens support session-based catchup streaming via session_start and session_end parameters, and accept additional query parameters through additional_querylist.

CDNVideo

secure_token_cdnvideo $cdnv_token {
    key 000102030405060708090a0b0c0d0e0f;
    acl "$secure_token_baseuri_comma*";
    end 1h;
}

Uses MD5-based signing with customizable parameter names (md5_param_name defaults to "md5", exp_param_name defaults to "e").

ChinaCache

secure_token_chinacache $cc_token {
    key 000102030405060708090a0b0c0d0e0f;
    key_id "mykey1";
    algorithm hmacsha256;
    end 1h;
}

Supports both hmacsha1 and hmacsha256 algorithms. The key_id parameter identifies which key the CDN should use for validation.

CHT

secure_token_cht $cht_token {
    key 000102030405060708090a0b0c0d0e0f;
    acl "$secure_token_baseuri_comma*";
    end 1h;
}

Uses MD5-based token signing with base64url encoding.

IIJ PTA

secure_token_iijpta $iij_token {
    key 000102030405060708090a0b0c0d0e0f;
    iv 00000000000000000000000000000000;
    end 1h;
}

Unlike the other formats, IIJ PTA uses AES-128-CBC encryption rather than HMAC signing. The token contains encrypted policy data rather than a signature. For a deeper look at period-of-time authentication, see the NGINX PTA module guide.

Generic Directives Reference

These directives control how tokens are applied and how cache headers are set. They can be placed in http, server, or location context.

Token Application

Directive Default Description
secure_token (none) Token value to embed (usually a variable like $akamai_token)
secure_token_types (none) MIME types that trigger tokenization
secure_token_uri_filename_prefix (none) Only tokenize URIs whose filename starts with this prefix
secure_token_avoid_cookies on Prefer query string over cookies for manifest types
secure_token_tokenize_segments on Tokenize segment URIs within manifests

Cache and Expiration Control

Directive Default Description
secure_token_expires_time (none) Cache expiration for non-tokenized responses
secure_token_cookie_token_expires_time (none) Cache expiration for cookie-tokenized responses
secure_token_query_token_expires_time (none) Cache expiration for query-tokenized responses
secure_token_cache_scope public Cache-Control scope for non-tokenized responses
secure_token_token_cache_scope private Cache-Control scope for tokenized responses
secure_token_last_modified Sun, 19 Nov 2000 08:52:00 GMT Last-Modified for non-tokenized responses
secure_token_token_last_modified now Last-Modified for tokenized responses

Content Type Overrides

Directive Default Description
secure_token_content_type_m3u8 application/vnd.apple.mpegurl MIME type parsed as M3U8
secure_token_content_type_mpd application/dash+xml MIME type parsed as MPD
secure_token_content_type_f4m video/f4m MIME type parsed as F4M

Time Format

All token start and end parameters accept flexible time formats:

Format Example Description
Relative 1h, 30m, 10d Offset from current time
Epoch keyword epoch Unix timestamp 0
Max keyword max Unix timestamp 2147483647
Absolute @1609459200 Specific Unix timestamp
Negative offset -5m Current time minus 5 minutes

NGINX Variables

The module exports three variables:

Variable Description
$secure_token_baseuri The $uri truncated to the last slash. For /a/b/c.htm, this is /a/b/
$secure_token_baseuri_comma Same as above, but also truncated at the first comma. For /a/b,c/d.htm, this is /a/b
$secure_token_original_uri The original encrypted URI before decryption (only set when URI encryption is active)

These variables are primarily used in acl parameters to construct path-based access control patterns.

URI Encryption (Advanced)

Beyond token authentication, the NGINX secure token module can encrypt segment URIs using AES-256-CBC. This prevents clients from guessing or manipulating segment paths, providing an additional layer of content protection. For a related approach using AES-256 for NGINX variable encryption, see the NGINX Encrypted Session module.

Important: URI encryption is designed to work with dynamic manifest generators such as the Kaltura VOD module. The module encrypts segment URIs in outgoing manifests and decrypts them when segments are requested. You should only enable URI encryption in locations where the initial request is handled by a VOD module or where you have a separate routing setup for encrypted and non-encrypted paths.

URI Encryption Configuration

location ~ ^/hls/entryId/([^/]+)/(.*) {
    # VOD module handles manifest generation
    vod hls;

    secure_token_encrypt_uri on;
    secure_token_encrypt_uri_key 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f;
    secure_token_encrypt_uri_iv 00000000000000000000000000000000;
    secure_token_encrypt_uri_part $2;
    secure_token_types application/vnd.apple.mpegurl;
}

URI Encryption Directives

Directive Default Description
secure_token_encrypt_uri off Enable URI encryption
secure_token_encrypt_uri_key (none) AES-256-CBC key (64 hex characters = 256 bits)
secure_token_encrypt_uri_iv (none) AES-256-CBC IV (32 hex characters = 128 bits)
secure_token_encrypt_uri_part (none) Which part of the URI to encrypt (for regex locations)
secure_token_encrypt_uri_hash_size 8 MD5 hash bytes prepended for integrity validation (0-16)

How URI Encryption Works

  1. The module computes an MD5 hash of the URI segment to be encrypted.
  2. It prepends the first N bytes of that hash (controlled by secure_token_encrypt_uri_hash_size) to the plaintext.
  3. It encrypts the combined data using AES-256-CBC.
  4. The ciphertext is base64url-encoded and substituted into the manifest.
  5. When a client requests the encrypted URI, the module reverses the process: base64url-decode, decrypt, validate the hash, and serve the original resource.

If the hash validation fails, the module returns HTTP 403 Forbidden, effectively blocking tampered or fabricated URIs.

Practical Example: Protecting a Live Stream with Token Overlay

A common use case is adding token security to an existing live stream served by an upstream origin. This works well in combination with the NGINX RTMP module for live streaming setups:

secure_token_akamai $token {
    key 000102030405060708090a0b0c0d0e0f1011121314151617;
    acl "$secure_token_baseuri_comma*";
    end 1h;
}

server {
    listen 443 ssl;
    server_name stream.example.com;

    location /live/ {
        proxy_pass http://origin.example.com;

        secure_token $token;
        secure_token_types application/vnd.apple.mpegurl application/dash+xml;

        secure_token_expires_time 100d;
        secure_token_query_token_expires_time 1h;
    }
}

In this configuration, NGINX proxies the live stream from the origin server and injects Akamai tokens into both HLS and DASH manifests on the fly. No changes to the origin server are required.

Testing Your Configuration

After configuring the NGINX secure token module, verify the token injection is working correctly.

Syntax Check

nginx -t

Verify Token Injection

Request a manifest and inspect the segment URLs:

curl -s https://stream.example.com/video/playlist.m3u8

You should see tokens appended to every segment URL as query string parameters:

#EXTINF:10.0,
segment0.ts?__hdnea__=st=...~exp=...~acl=...~hmac=...

Verify Cache Headers

Tokenized responses should have appropriate cache headers:

curl -sI https://stream.example.com/video/playlist.m3u8

Expected headers for tokenized responses:

Cache-Control: private, max-age=3600, max-stale=0
Last-Modified: <current timestamp>

The Cache-Control: private scope prevents CDN edge caches from serving stale tokenized manifests to different clients.

Security Best Practices

Use Strong Keys

Generate cryptographically random keys for production use:

openssl rand -hex 24

This produces a 48-character hex string (192 bits), suitable for Akamai tokens. Never use example or test keys in production.

Set Short Token Lifetimes

Keep token expiration as short as your use case allows. For live streaming, end 1h is reasonable. For on-demand content, end 30m may be sufficient. Shorter lifetimes reduce the window for token reuse if a URL is shared.

Bind Tokens to IP Addresses

When your audience uses direct connections (not behind shared NATs or corporate proxies), bind tokens to the client IP:

secure_token_akamai $token {
    key <your-key>;
    acl "$secure_token_baseuri_comma*";
    ip_address $remote_addr;
    end 30m;
}

Protect Your Signing Keys

Store private keys and hex secrets outside the web root with restricted permissions:

chmod 600 /etc/nginx/keys/cloudfront-private.pem
chown root:root /etc/nginx/keys/cloudfront-private.pem

Use HTTPS

Token authentication is pointless over plain HTTP. Tokens transmitted in clear text can be intercepted and reused. Therefore, always serve tokenized content over TLS.

Performance Considerations

The NGINX secure token module has minimal performance overhead because:

For very high-throughput scenarios, consider these optimizations:

Troubleshooting

Tokens Not Appearing in Manifests

Verify that secure_token_types includes the correct MIME type for your manifests. For HLS, this is application/vnd.apple.mpegurl. Check the Content-Type header of your manifest response:

curl -sI https://stream.example.com/video/playlist.m3u8 | grep Content-Type

If the content type does not match, use secure_token_content_type_m3u8 to override the expected type.

“Unknown Directive” Error

The secure_token_akamai, secure_token_cloudfront, and other CDN block directives must be placed in the http context, not inside server or location. Additionally, ensure the module is loaded via load_module at the top of nginx.conf.

CloudFront Tokens Silently Failing

On RHEL 9+, Rocky Linux 9+, and AlmaLinux 9+, CloudFront tokens may silently produce empty values with no error in the NGINX log. This happens because CloudFront token signing uses SHA-1 internally, which the DEFAULT system crypto policy on these distributions disables for digital signatures. The fix is to switch to the LEGACY crypto policy:

sudo update-crypto-policies --set LEGACY
sudo systemctl restart nginx

You can confirm the issue by testing SHA-1 signing on your system:

openssl dgst -sha1 -sign /path/to/key.pem /dev/null

If this command fails with invalid digest, CloudFront tokens will not work until the crypto policy is changed.

400 Bad Request with URI Encryption

If secure_token_encrypt_uri is enabled and a request contains a plaintext (non-encrypted) URI, the NGINX secure token module will return 400 Bad Request. This is expected behavior — URI encryption requires all segment requests to use encrypted paths. Ensure your manifest generator (such as the Kaltura VOD module) provides encrypted URIs.

Conclusion

The NGINX secure token module provides a robust, edge-level solution for protecting streaming content with CDN-compatible token authentication. By generating tokens directly within NGINX, you avoid the latency and complexity of external authentication services while maintaining compatibility with major CDN providers.

Whether you use Akamai’s HMAC-SHA256 tokens, CloudFront’s RSA-signed policies, or any of the five other supported formats, the module handles manifest parsing, token injection, and cache header management automatically.

For the complete directive reference and source code, visit the module’s GitHub repository. The module is available as a pre-built package from the GetPageSpeed repository for both RPM-based and APT-based distributions.

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