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:
- Application-layer authentication (session cookies, JWT headers) protects the initial page load but not the individual segment requests that a video player makes in the background.
- NGINX’s built-in secure_link module can sign URLs, but it does not understand manifest formats and cannot rewrite segment URLs inside HLS or DASH playlists.
- CDN-side token validation requires tokens in a CDN-specific format (Akamai, CloudFront, etc.), which your origin server must generate and inject into every segment URL within every manifest response.
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:
- Token definition: You define a named token block in the
httpcontext (for example,secure_token_akamai $my_token { ... }). This block specifies the signing key, access control list (ACL), and expiration time. - Token binding: In the
locationblock that serves your manifests, you bind the token variable withsecure_token $my_tokenand specify which content types trigger tokenization viasecure_token_types. - 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.
- Non-manifest responses: For content types that are not manifests (such as
.tsvideo segments), the token is delivered as an HTTPSet-Cookieheader 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.
Token Delivery: Query String vs. Cookie
By default, the module prefers query string tokens for manifest types (secure_token_avoid_cookies is enabled). This means:
- M3U8, MPD, F4M manifests receive tokens as query string parameters appended to each segment URL inside the manifest body.
- All other content types (such as
.tssegments or.mp4files) receive tokens asSet-Cookieheaders.
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_moduledirective 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 nginxThis 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
- The module computes an MD5 hash of the URI segment to be encrypted.
- It prepends the first N bytes of that hash (controlled by
secure_token_encrypt_uri_hash_size) to the plaintext. - It encrypts the combined data using AES-256-CBC.
- The ciphertext is base64url-encoded and substituted into the manifest.
- 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:
- Token generation is fast. HMAC-SHA256 (Akamai) and MD5 (CDNVideo, CHT) are computationally cheap. RSA signing (CloudFront) is more expensive but still manageable at typical streaming request rates.
- Body filtering is efficient. The manifest parser uses a state machine that processes the response body in a single pass without buffering the entire document.
- No upstream requests. Token generation happens entirely within NGINX — there are no additional HTTP calls to authentication services.
For very high-throughput scenarios, consider these optimizations:
- Set
secure_token_tokenize_segments offif you only need tokens on specific URI prefixes (controlled bysecure_token_uri_filename_prefix). This reduces the number of URLs the module must rewrite. - Use Akamai tokens (HMAC-SHA256) instead of CloudFront tokens (RSA) when possible, as HMAC operations are significantly faster than RSA signing.
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.

