Why this matters

You have a streaming product. Some of it is paid, some of it is geo-restricted, some of it is for logged-in users only. Putting "is this user allowed?" logic on your origin works for ten thousand views per day and collapses under a million per minute. Every serious CDN solves this by signing the URL at the edge — and every serious team gets it wrong at least once. This article is for the engineer scoping that work, the product manager comparing vendors, and the architect who has been told "just turn on signed URLs" without being told what breaks next.

What "signed URL" actually means

A signed URL is a normal HTTP URL plus three extra pieces of evidence: a policy (what the bearer is allowed to do), an expiry (when the permission ends), and a signature (a cryptographic hash that ties the policy and expiry to a secret only you and the CDN share). Anyone can read the URL. Nobody who does not hold the secret can forge a new one.

The two flavours of signature in production are HMAC-SHA256 and RSA-SHA256 (often called RS256 in JWT land). HMAC uses a single shared secret on both ends — fast to compute, cheap to store, and the default for Akamai EdgeAuth, Google Media CDN, Fastly's signed URL recipes, Cloudflare Stream's signing-key flow, and most NGINX-based CDNs. RSA uses a key pair — you sign with the private key, the CDN verifies with the public key. CloudFront and Mux take the RSA route because it lets you rotate the verifier (the public key in the CDN's "key group") independently from the signer (your application server). Both flavours produce an opaque blob attached to the URL; the difference matters only when you design key rotation.

A worked example. The cleartext to be signed is the canonical request the viewer will make:

URL path:      /vod/customer42/manifest.m3u8
Expires:       1748185200            (Unix epoch seconds, ≈ 2025-05-25 13:00 UTC)
Allowed IP:    203.0.113.7/32
Customer:      customer42

You hash that string with your secret key and SHA-256, then base64-encode it. The CDN receives the request, repeats the same hash with the same secret, and compares. One bit different anywhere — wrong path, wrong expiry, IP outside the range — and the request is rejected at the edge.

Diagram titled Signed URL anatomy: four parts every CDN signs, and the check it repeats at the edge. Card 1, canonical request: URL path /vod/customer42/manifest.m3u8, Expires 1748185200 (Unix seconds, 2025-05-25 13:00 UTC), allowed IP 203.0.113.7/32, customer customer42; the facts the signature binds together. An arrow leads from it down to card 3. Card 2, secret: a shared HMAC key or an RSA private key, held by the app server only; an arrow leads right to card 3. Card 3, compute signature: HMAC-SHA256(request, secret) or RSA-SHA256 sign(request), then base64-encode the result. An arrow leads down to card 4, the final URL the player fetches: https://cdn.example.com/vod/c42/manifest.m3u8?Expires=1748185200&Signature=ZTk...&Key-Pair-Id=K1AB. An arrow leads down to the orange card, CDN edge: the same check on every request. a, read the fields from the URL and the client IP; b, recompute the signature with the same secret; c, compare, and one bit different means 403 at the edge. The origin never sees a rejected request. Legend: what you hold, what you compute, what the CDN checks. Figure 1. The four moving parts every signed-URL scheme shares.

The five CDN dialects, side by side

The vocabulary differs, the algorithm is the same. The table below is the part most teams wish they had on day one.

CDNToken formatAlgorithmPlacementWhere the spec lives
Amazon CloudFrontCanned or custom policy + signatureRSA-SHA1 (legacy) or RSA-SHA256 via key groupsQuery string (?Policy=…&Signature=…&Key-Pair-Id=…) or cookies (CloudFront-Policy, -Signature, -Key-Pair-Id)AWS CloudFront Developer Guide, "Use signed URLs" and "Set signed cookies using a custom policy"
Akamai Adaptive Media Delivery (EdgeAuth)ACL + expiry + nonce + HMACHMAC-SHA256 (default)Query string, cookie, or header — configurable per-propertyAkamai TechDocs, "Enable Token Authentication"
Cloudflare StreamJWT (RS256)RSA-SHA256, generated via API token, signing key, or Worker bindingPath component (/<token>/manifest.m3u8)Cloudflare Stream docs, "Secure your Stream"
Google Media CDNDual-token: short HMAC for path + long cookieHMAC-SHA1 / HMAC-SHA256 / Ed25519Query string + cookieGoogle Cloud "Edge cache origin → signed requests"
Mux (and most JWT-first vendors)Signed JWT, RS2562048-bit RSA key pairQuery string (?token=<JWT>)Mux "Secure video playback" guides
Three nuances that bite teams later:

The reserved query-parameter names differ by vendor. CloudFront removes Key-Pair-Id, Policy, and Signature before forwarding to the origin, but it forwards any name suffixed with -PREFIX, which is the escape hatch when your application also needs a parameter named, say, Policy. Akamai's default is hdnts; Google Media CDN uses edge-cache-token; Cloudflare Stream puts the JWT into the path, not the query string, specifically to keep the URL cacheable.

The IPv6 trap. CloudFront's custom policy with an IpAddress field is a v4-only construct — if you enable IPv6 on the distribution, the IP check silently breaks. AWS documents this; teams find it three months in, when an iOS rollout exposes the first IPv6-only carrier.

The signature algorithm is migrating. CloudFront's "trusted key groups" flow uses RSA-SHA256; the older "trusted signers" flow used RSA-SHA1 (acceptable, but on the way out). Akamai's HMAC-SHA1 is still supported and still in field; new properties should default to HMAC-SHA256.

Signed URL or signed cookie — for a streaming workload, both

For a single download, a signed URL is enough. For HLS or DASH, the player will fetch one master playlist, several media playlists, a key URI, and hundreds or thousands of segments. Rewriting every URL in the manifests with a per-request signature is one option; setting a signed cookie once at session start and letting the player fetch everything inside the protected origin path is the other.

CloudFront's own documentation is direct: signed cookies are the right pick when you protect "many files" or want to keep the URLs themselves clean and cacheable. Akamai's session-token model is essentially the same idea — an access token validates a session, then a "long" session token rides in a cookie or query string for the rest of the stream. The trade-off: cookies require third-party cookie support, which fails on some smart-TV browsers and on iOS Safari for cross-domain players. The hybrid pattern — signed cookie for manifests and keys, signed URL on the master playlist to bootstrap the session — is the default in production today.

If you sign segment URLs individually, sign the common prefix rather than the full URL of each segment. CloudFront and Akamai both support this with a wildcard or "ACL" — one signature covers /customer42/title-9000/* for one hour, so the player does not request a fresh signature per segment and the CDN cache key remains stable. Signing each segment URL is the second most common reason teams blow up their cache hit ratio after badly designed cache keys.

Table titled Signed URL vs signed cookie: one HLS session in five steps, signing every request or signing the session. Step 1: with a signed URL the player asks the app server for the master URL and the app signs path, expiry and IP; with a signed cookie the app server answers with Set-Cookie carrying CloudFront-Policy, -Signature and -Key-Pair-Id. Step 2: the player loads master.m3u8 with the token and the edge verifies it; or loads master.m3u8 with the cookie riding along, so manifest URLs stay clean. Step 3: each variant playlist needs its own token, placed in the master or fetched fresh; or every variant and segment uses the same cookie, with no rewriting and stable cache keys. Step 4: each segment needs a token or a wildcard ACL, so sign a prefix, not each segment; or cookie expiry ends the session and the app server sets a new cookie. Step 5, in orange for signed URLs: a bitrate switch can hit an unsigned variant, a common production failure; the same cookie covers key and DRM endpoints, and cross-domain cookie rules apply. Fits when: single download, master playlist bootstrap, no cookies; or HLS or DASH on one player domain whose browsers take cookies. Watch out for: cache-key collisions, expired tokens, per-segment signing; or cross-origin cookies, iOS Safari ITP, smart-TV quirks. Bottom row: the production default is the hybrid, a signed URL on the master playlist bootstraps the session and a signed cookie covers keys and segments. Figure 2. The two flows side by side for one HLS playback session.

Token expiry and the mid-session refresh problem

Tokens are short-lived on purpose: a leaked URL should stop working in minutes, not days. For VOD that finishes in 15 minutes, a 30-minute expiry is fine. For a 3-hour live concert or a 90-minute movie, the token will expire mid-playback and the next playlist refresh or segment fetch will get a 403.

There are three robust patterns. Long-enough expiry is the simplest: set the token to outlive the longest plausible session, accept the slightly larger window of misuse. Token renewal keeps a short expiry and refreshes the signed URL from your application server before it expires; in hls.js this is done by listening for the MANIFEST_LOADED event, then either calling hls.loadSource() with a new URL or rewriting requests in xhrSetup to attach a fresh token. Path-only signature signs only the base path and lets a separate, short-lived "session token" cookie carry the time bound, so the path signature stays stable for the full title.

The pitfall worth naming is bitrate switching. Akamai's hls.js bug 1450 — still cited in the community — is exactly this: an Akamai token signs /master.m3u8 and the four variant playlists referenced inside it; when the player switches up to the 5 Mbps rendition for the first time, it requests a fifth variant playlist whose URL was never visited and whose token may have expired. The fix is to sign the common ACL (/title-9000/*) rather than individual playlists, and to renew the token before the longest expected viewing session.

Origin protection — the other half of the job

A signed URL on the CDN does nothing if a curious user can hit the origin directly. The locking pattern is layered.

The origin sees only CDN traffic. CloudFront does this via Origin Access Control (OAC, the modern replacement for OAI) on S3, or via signed origin headers on a custom HTTP origin. Akamai's Site Shield publishes an IP list the origin firewall accepts. Cloudflare uses Authenticated Origin Pulls with a client certificate; Google Cloud CDN uses Cloud Armor plus a X-Edge-Cache-Token shared secret. The principle: the origin's network ACL rejects everything not coming from the CDN's edges.

Mutual TLS between CDN and origin is the upgrade for paid-tier traffic. The CDN presents a client certificate; the origin validates it; intermediaries cannot pretend to be the CDN. AWS, Akamai, Fastly, and Cloudflare all support this on the enterprise tier.

Authenticated origin headers. The CDN attaches a header — CloudFront-Key-Pair-Id, a custom HMAC, or a JWT — that the origin verifies on every request. Belt and braces: even if someone scrapes the origin IP, requests without the header are dropped.

The reverse question — "what if the CDN itself is compromised?" — is what DRM and forensic watermarking exist for. That is a different layer; this article ends at the segment leaving the CDN.

Diagram titled Origin protection: defence in depth. The signed URL locks the front door; three more layers guard the origin. Top row, left to right with arrows: Viewer, player plus token in URL or cookie; CDN edge, layer 1, verifies the token; Origin, layers 2 to 4, no direct way in. The public internet cannot reach the origin directly. Below, the four layers, outside in. 1, edge token: signed URL or signed cookie, the CDN drops what it cannot verify; first and cheapest defence, checked on every request. 2, origin network ACL: the origin accepts only CDN traffic and the rest dies at the firewall; AWS OAC, Akamai Site Shield, Cloudflare Authenticated Origin Pulls. 3, mutual TLS between CDN and origin: the CDN presents a client certificate and the origin validates it, so nobody in between can pose as the CDN; enterprise tiers. 4, authenticated origin header: the CDN adds a JWT or HMAC header and the origin checks each request; belt and braces if the origin IP ever leaks. Figure 3. Defence in depth from edge token to origin firewall.

The cache-key trap (or, how a signed URL silently destroys your hit ratio)

This is the single most common production failure with signed URLs, and it costs real money.

A CDN caches an object under a cache key — a deterministic function of the URL and a subset of headers and query strings. When the signature ends up inside the cache key, every viewer gets a unique cache key for the same object, the cache hit ratio drops from 99% to roughly 0%, the origin egress bill explodes, and the team finds out from the finance dashboard, not the engineering one.

Three rules keep you out of trouble. Strip the signature parameters from the cache key. CloudFront does this by default for Policy, Signature, and Key-Pair-Id; Akamai's default hdnts is excluded; for any other vendor or any custom parameter name, verify and configure. Use signed cookies for shared assets, because cookies are not in the default cache key. Sign the common path, not the unique URL — the cache key stays the same across viewers because the underlying URL is the same.

The second trap inside this trap: cache lifetime longer than token lifetime. CloudFront will happily serve a cached 200 response after the signature in the original URL has expired, because the cache key doesn't include the expiry. For paid content, set the CDN's max-age short enough that re-signing happens at least once per cache lifetime, or rely on cookie-based sessions where the cookie itself carries the time bound.

Common mistakes

Pitfall: signing every segment URL individually. The cache hit ratio collapses and the CDN bill triples. Sign the common ACL or use a signed cookie at the session level.

Three more failure modes worth flagging once. Storing the signing secret in the front-end (it appears in view-source). Setting the same expiry for everyone (a single replay attack works for the full window — pin to IP, user ID, or device). Skipping the origin-protection step because "the signed URL is enough" (it isn't — without origin ACL, the origin URL is one whois lookup away from being scraped).

↓ Free download: Signed URL Hardening ChecklistOne page with the 12 configuration items to verify before a paid-streaming launch: signature algorithm, cache key, the IPv6 trap, origin lock and the token-renewal flow.

Where Fora Soft fits in

Most of our streaming, OTT, telemedicine, and e-learning projects start with the same conversation: the team has a working video pipeline and is about to put it behind a paywall, a regional licence, or a clinician-only access list. We design the signed-URL flow against the customer's CDN of choice, wire the origin lockdown (OAC, Site Shield, Authenticated Origin Pulls, or a custom edge token), and instrument the player so token-expiry events surface as alerts rather than viewer complaints. The same scaffolding underpins our video-conferencing and surveillance work, where the access list is per-room or per-camera rather than per-asset. Our OTT and IPTV development team builds the paywall, entitlement and signed-URL layer for subscription services, and our scalable streaming architects lock down origins across multi-CDN delivery.

Video Streaming: A Complete Guide to Video Delivery: From ABR and CDNs to Players, WebRTC, DRM, and the Economics of Streaming - book cover

The book

Video Streaming: A Complete Guide to Video Delivery: From ABR and CDNs to Players, WebRTC, DRM, and the Economics of Streaming

Signed URLs are one lock on a paid stream. Book 3 walks the whole delivery path around it: CDN cache keys and origin shielding, the players that have to renew tokens mid-session, multi-DRM and forensic watermarking for when the CDN itself is not enough, and the egress bill that shows up when the cache key goes wrong. Written by Nikolay Sapunov, CEO at Fora Soft.

Read it on Amazon →Kindle & paperback · Fora Soft Video Engineering Handbook

Putting your streams behind a paywall or a login?

Tell us your CDN, your player and who should see what. We will design the signing scheme, check that signatures stay out of the cache key, plan token renewal for long sessions and lock the origin so nobody can go around the edge. 250+ video products shipped since 2005.

Book a 30-min call →WhatsApp →Email us →

See our case studies

References

  1. Amazon Web Services. CloudFront Developer Guide — Use signed URLs. https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html (accessed 2026-05-24).
  2. Amazon Web Services. CloudFront Developer Guide — Create a signed URL using a custom policy. https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html (accessed 2026-05-24).
  3. Amazon Web Services. CloudFront Developer Guide — Decide to use signed URLs or signed cookies. https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-choosing-signed-urls-cookies.html (accessed 2026-05-24).
  4. Amazon Web Services. Amazon CloudFront for Media — Best Practices for Streaming Media Delivery. AWS whitepaper PDF.
  5. Akamai Technologies. Adaptive Media Delivery — Enable Token Authentication. https://techdocs.akamai.com/adaptive-media-delivery/docs/enable-token-authentication (accessed 2026-05-24).
  6. Akamai Technologies. Adaptive Media Delivery — Add Token Authentication. https://techdocs.akamai.com/adaptive-media-delivery/docs/add-token-auth (accessed 2026-05-24).
  7. Cloudflare. Cloudflare Stream — Secure your Stream. https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/ (accessed 2026-05-24).
  8. Cloudflare. Cloudflare Stream API — Create signed URL tokens for videos. https://developers.cloudflare.com/api/resources/stream/subresources/token/methods/create/ (accessed 2026-05-24).
  9. Mux. Secure video playback — guides and JWT signing. https://www.mux.com/docs/guides/secure-video-playback and https://www.mux.com/docs/guides/signing-jwts (accessed 2026-05-24).
  10. Google Cloud / Sinha, V. Protecting HLS Streaming with Google Media CDN Dual Token authentication using HMAC tokens. Google Cloud Community on Medium (accessed 2026-05-24).
  11. IETF. RFC 8216 — HTTP Live Streaming, August 2017, §4.4.4 EXT-X-KEY (controls the URI from which AES-128 key material is fetched; same URI can carry an authentication token).
  12. IETF. draft-pantos-hls-rfc8216bis-22 — HTTP Live Streaming 2nd Edition, 2026 (work in progress; subject to change before RFC publication).
  13. Apple. HTTP Live Streaming (HLS) Authoring Specification for Apple devices, revision 2025-09 (accessed 2026-05-24).
  14. video-dev/hls.js. Issue #1450 — Akamai HLS with Auth Tokens expire. Therefore bitrate switching fails. GitHub.
  15. video-dev/hls.js. API documentation — xhrSetup config option for custom XHR setup including auth headers. https://github.com/video-dev/hls.js/blob/master/docs/API.md (accessed 2026-05-24).
  16. aws-samples. amazon-cloudfront-protecting-hls-manifest-with-signed-url. GitHub reference implementation.