Modern CDNs do more than cache. They act as a distributed authorization layer, deciding on every request whether to serve, redirect, or reject. Request signing is the foundation of that layer. Unlike cookies or basic auth, signed requests work seamlessly with cache, support anonymous users, and survive across any client without headers. For premium video, private assets, software packages, and API acceleration, signing is non-negotiable.
This guide compares the three dominant patterns — HMAC signed URLs, JWT edge auth, and URL token authentication — with production-ready implementation details, trade-offs, and operational pitfalls we see at scale.
Why Request Signing Matters at the Edge
Without signing, your CDN is an open proxy to your origin. Attackers can bypass paywalls by guessing sequential URLs, embed your high-bandwidth video on their site, or brute-force private S3 origins through the CDN. Signed requests enforce three guarantees simultaneously:
- Authenticity: The URL was generated by someone holding the secret key, typically your application server or serverless function.
- Integrity: No parameter (path, expiry, IP, quality) was tampered with after signing. A single byte change invalidates the signature.
- Temporality: The link self-expires. Even if leaked, its blast radius is bounded by TTL.
Critically, validation happens before cache lookup. A valid signature results in a cache HIT or MISS; an invalid one returns a 403 at the edge without ever contacting origin. This protects origin from both abuse and cost. When combined with DDoS protection strategies, request signing dramatically reduces the attack surface for volumetric and application-layer attacks.
Signing also preserves cache efficiency. Unlike Authorization headers which often bypass cache, signed URLs are cache keys. Two users with different tokens requesting /video/1080p/segment_42.ts?exp=...&sig=... will not share cache, but users with the same signed URL will. Design your signing scope carefully to balance security and hit ratio.
1. HMAC-Based Signed URLs: Lightweight and Fast
HMAC is the workhorse for CDN URL signing. You concatenate a canonical string — usually path + expiry + optional IP + optional key ID — and sign it with HMAC-SHA256 using a symmetric secret. The signature is appended as a query parameter. Validation is a single hash comparison.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
HMAC is ideal when you need minimal overhead, language-agnostic verification, and maximum performance. CloudFront Signed URLs, Fastly URL signing, and Akamai Token Auth all use this primitive under the hood.
Canonical Signing Pattern
import hmac, hashlib, base64, time
SECRET = b'your-256-bit-secret'
KEY_ID = "v1"
def sign_url(path: str, expires: int, client_ip: str = "") -> str:
# Canonical string must be deterministic and unambiguous
data = f"{path}:{expires}:{client_ip}:{KEY_ID}".encode()
sig = hmac.new(SECRET, data, hashlib.sha256).digest()
token = base64.urlsafe_b64encode(sig).decode().rstrip("=")
return f"{path}?exp={expires}&kid={KEY_ID}&sig={token}"
# Example: 5-minute URL bound to viewer IP
url = sign_url("/assets/premium/report.pdf", int(time.time())+300, "203.0.113.45")
# -> /assets/premium/report.pdf?exp=172529...&kid=v1&sig=4fX...
Edge Validation (VCL / Nginx)
# Nginx + secure_link (HMAC-SHA256)
secure_link $arg_exp,$arg_sig;
secure_link_md5 "$uri $arg_exp $remote_addr v1 your-256-bit-secret";
if ($secure_link = "") { return 403; }
if ($secure_link = "0") { return 410; } # expired
# VCL equivalent: recompute HMAC and compare in constant time
Key design decisions: include the key ID (kid) to support rotation, bind to IP only when necessary (it breaks NAT and mobile), and always use constant-time comparison to prevent timing attacks. Keep the canonical string format versioned. Changing it later invalidates all historical URLs.
For content where cache hit ratio is critical, review your caching strategy to ensure signed parameters are part of the cache key but not over-fragmented. Exclude the signature from cache key normalization if your CDN supports it.
2. JWT-Based Authentication at the Edge
JWTs move identity to the edge. Instead of signing just a URL, you sign a JSON payload containing claims: sub, exp, path, ip, acl, and custom entitlements. The token is passed as a query parameter, cookie, or Authorization: Bearer header and validated by the CDN using a shared secret (HS256) or public key (RS256/ES256).
JWTs shine when you already have an identity provider and need fine-grained access. A single JWT can authorize /videos/* for a subscriber tier without minting a new URL for every segment — essential for HLS/DASH where a manifest references hundreds of segments.
// Node.js: Mint JWT for CDN (HS256)
import jwt from 'jsonwebtoken';
const token = jwt.sign({
sub: "user_8921",
exp: Math.floor(Date.now()/1000) + 600, // 10 min
path: "/videos/premium/*",
ip: "203.0.113.45",
tier: "pro"
}, SECRET, { algorithm: 'HS256', keyid: 'v1' });
// URL: https://cdn.example.com/videos/premium/intro.m3u8?token=${token}
// Edge validation (Cloudflare Workers / Fastly Compute)
const payload = await jwtVerify(token, SECRET);
if (payload.path !== requestPath && !minimatch(requestPath, payload.path))
return new Response('Forbidden', {status: 403});
Use RS256 or ES256 when the CDN validates but does not mint tokens. The origin holds the private key; the edge only needs the public key, limiting blast radius if an edge node is compromised. ES256 is 40% faster to verify than RS256 at equivalent security.
Beware JWT bloat. A typical JWT is 300-600 bytes, which inflates URLs and may exceed header limits. Keep claims minimal, avoid embedding large ACLs, and prefer short-lived tokens. Always validate exp, nbf, and aud if present. For observability, log the kid and jti (JWT ID) to correlate denylists, as discussed in CDN monitoring and observability.
3. URL Token Authentication: CDN-Native Tokens
URL Token Authentication is a vendor-specific implementation that wraps HMAC with additional controls: path wildcards, IP CIDR, country, referer, and protocol enforcement. Akamai EdgeAuth, CloudFront Signed Cookies, and Bunny Token Authentication fall here. You generate a token that encodes policy rather than just a signature.
Example token structure (decoded): exp=1725300000~acl=/premium/*~ip=203.0.113.0/24~hmac=.... The CDN parses and enforces each field natively, without custom Worker code.
# Python: Akamai-style token
import hmac, hashlib
def akamai_token(acl, exp, ip, secret_hex):
secret = bytes.fromhex(secret_hex)
field = f"exp={exp}~acl={acl}~ip={ip}"
h = hmac.new(secret, field.encode(), hashlib.sha256).hexdigest()
return f"{field}~hmac={h}"
token = akamai_token("/premium/*", 1725300000, "203.0.113.45", "a1b2c3...")
url = f"https://cdn.example.com/premium/video.m3u8?token={token}"
URL tokens are powerful for marketing campaigns and partner distribution where you need to restrict by referer or geo without writing edge logic. The downside is vendor lock-in; token format is not portable. If you anticipate multi-CDN, abstract token generation behind an internal service that can emit different formats from one policy.
Tokens also enable signed cookies for HLS. Instead of signing every segment URL, you set a __Secure-Token cookie once after authentication; the player requests segments normally and the edge validates the cookie. This improves cache hit ratio and simplifies players.
Comparison: HMAC vs JWT vs URL Token
| Criterion | HMAC Signed URL | JWT | URL Token (Vendor) |
|---|---|---|---|
| Payload | Hash only, no claims | JSON claims (sub, tier, acl) | Encoded policy string |
| Size | ~45 bytes (Base64) | 300–600 bytes | 120–250 bytes |
| Validation | 1x HMAC-SHA256 | Base64 + JSON + verify | Native CDN parser |
| Latency | ~0.05ms | ~0.2–0.5ms | ~0.05ms (native) |
| Best For | Static assets, images, downloads | Video with entitlements, APIs | Geo/referer-restricted campaigns |
| Portability | High (any edge) | High (standard) | Low (vendor-specific) |
| Revocation | Key rotation only | jti denylist + rotation | Key rotation / token blacklist |
Implementation Best Practices
Getting signing right is less about cryptography and more about operational discipline. Follow these rules from production incidents we have reviewed:
1. Use Separate Keys per Environment and Scope
Never reuse the same secret for staging and production, or for video and downloads. Scope keys by purpose (video-prod-v1, assets-prod-v1) so rotation can be surgical. Store secrets in a KMS or secrets manager, not environment variables, and expose only the key ID to the edge.
2. Enforce Clock Skew and Constant-Time Checks
Allow 30–60 seconds of skew between generator and edge to handle NTP drift. Reject tokens with exp far in the future (e.g., >24h) to prevent abused long-lived URLs. Always compare signatures in constant time; naive string equality leaks timing side channels.
3. Sign the Minimal Canonical String
Include only what you need: normalized path (lowercased, decoded), expiry, and optionally IP or method. Do not sign the full query string unless you normalize sorting, or attackers can reorder parameters to bypass validation. Document the canonicalization precisely.
IP binding improves security but breaks legitimate users behind CGNAT, VPNs, or mobile handoffs. Prefer short TTL + token binding over strict IP checks for consumer content. Use IP binding for enterprise or internal distribution where IPs are stable.
4. Cache Correctly
Configure Vary and cache keys to include the token only when necessary. For HLS with JWT cookies, cache by path without the token and validate the cookie separately to maximize hits. For signed URLs, ensure the CDN caches 403 responses for only a few seconds to avoid caching a bad signature for legitimate users after rotation. See performance and security benefits of a CDN for how signing interacts with Tiered Cache and Origin Shield.
Key Rotation and Revocation Without Downtime
Rotation is where most teams fail. A hard secret change instantly invalidates all outstanding URLs, causing playback failures. Implement versioned keys:
- Generate new secret as
v2, deploy to edge alongsidev1(edge accepts both). - Update your application to sign new URLs with
v2only. - Wait until max TTL of
v1URLs expires (e.g., 24h). - Remove
v1from edge.
Automate this with your CI/CD and CDN API. For JWT with RS256, publish a JWKS endpoint (/.well-known/jwks.json) and let the edge fetch and cache public keys. Rotate by adding a new kid to JWKS, then retiring the old one after expiry.
For instant revocation, maintain a denylist of jti or token hashes in an edge KV store (Cloudflare Workers KV, Fastly Edge Dictionary, Akamai EdgeKV). On validation, check the denylist before checking the signature. Keep TTL on denylist entries equal to token expiry to bound storage. This pattern is essential for logout or subscription cancellation.
If you run multi-CDN, synchronize rotation across providers via a central signing service. That service becomes the single source of truth, emitting the correct token format per CDN from one policy engine, which aligns with edge computing architecture principles of pushing logic to the edge while keeping control centralized.
Monitoring and Debugging Signed Requests
Signing failures are invisible to users — they just see 403s. Instrument aggressively. Log at the edge: kid, validation result, failure reason (expired, bad sig, IP mismatch), and cache status. Export to your observability pipeline and alert on spikes in 403 rate, which often indicate clock skew or a bad deploy rather than an attack.
Provide a debug header in staging (X-Signature-Debug: exp=... computed=... received=...) but never in production. Use synthetic monitors that mint a valid URL and assert 200, plus a monitor with an intentionally invalid signature asserting 403. This catches misconfigured secrets before customers do.
Finally, treat signing as part of your security posture, not just access control. Combine it with bot management, WAF, and rate limiting for defense in depth.
When implemented correctly, HMAC gives you speed, JWT gives you identity, and URL tokens give you turnkey policy enforcement. Most mature architectures use HMAC for static assets and JWT cookies for video — getting the best of both worlds while keeping edge validation under a millisecond.
Frequently Asked Questions
HMAC signed URLs use a symmetric key to hash URL components like path and expiry, resulting in a compact ~45-byte signature. JWTs are self-contained tokens with a JSON payload that can carry user identity, roles, and ACLs. HMAC is faster and ideal for simple asset protection; JWT is better when you need rich context at the edge, such as authorizing all segments for a subscriber tier with one token.
Use the shortest TTL that preserves UX. For paywalled video or downloads, 60–300 seconds is typical. For image previews, 5–15 minutes. For private software distribution, up to 24 hours may be acceptable if combined with IP binding. Always allow 30–60 seconds of clock skew and avoid issuing tokens valid far in the future.
Stateless signatures cannot be revoked without state. Enable revocation by versioning keys with a key ID and rotating them to invalidate all URLs from that version, or by maintaining a denylist of token IDs (jti) in an edge KV store checked on every request. Short TTLs plus re-issuance also limit exposure without explicit revocation.
Validate at the edge. Edge validation blocks invalid requests before they reach origin or pollute cache, reduces latency, and saves origin cost. Return 403 directly from the edge on failure. Use origin validation only as a secondary check for highly sensitive operations.