Open Calculator

Securing Private Media Delivery: Presigned S3 URLs vs CloudFront Signed Cookies vs Edge HMAC Tokens

Published September 01, 2026 · Cryptographic Edge Access Control

Leaving an object storage bucket or CDN distribution unauthenticated exposes your infrastructure to unauthorized hotlinking, automated competitor scraping, and denial-of-wallet bandwidth attacks. However, securing shared media assets (such as paid online course videos, SaaS team attachments, or gated software downloads) using naive S3 SigV4 Presigned URLs introduces a costly architectural flaw: it destroys shared CDN edge caching.

This specification compares four cryptographic delivery architectures and demonstrates how to validate and strip HMAC tokens at the CDN edge so 10,000 authenticated users share a single cached object.

Why Native S3 SigV4 Presigned URLs Destroy CDN Cache-Hit Ratios

When your backend generates an AWS S3 or Cloudflare R2 SigV4 Presigned URL (`GetObjectCommand` with `getSignedUrl`), the signature embeds the exact generation timestamp (`X-Amz-Date=20260901T142205Z`) and a unique cryptographic hash (`X-Amz-Signature=...`) into the query string.

// Why Two Users Requesting the Same 500 MB Video Miss the CDN Cache
User 1 URL: /course/lesson1.mp4?X-Amz-Date=20260901T120001Z&X-Amz-Signature=8f3a...
User 2 URL: /course/lesson1.mp4?X-Amz-Date=20260901T120004Z&X-Amz-Signature=2c9b...
Result: If you strip query strings at the CDN, S3 rejects the origin request with HTTP 403. If you forward query strings, every user triggers a 100% cache miss!

Rule of Thumb: Use native S3 SigV4 Presigned URLs strictly for **single-user private files** (e.g., a user downloading their own private PDF tax invoice or uploading a profile avatar via `PutObject`). Never use S3 SigV4 Presigned URLs for one-to-many media or software assets served behind a CDN.

Cryptographic Media Authentication Architecture Matrix

Authentication Method Verification Point Shared CDN Cache Hit? Supports Multi-Segment HLS?
S3 SigV4 Presigned URL Origin Storage Bucket No (0% Shared Cache) No (Requires signing every `.ts` chunk)
CDN Signed URL (RSA / HMAC) CDN Edge PoP Yes (~98% Cache Hit) Partial (Requires token propagation)
CDN Signed Cookies (`Path=/video/*`) CDN Edge PoP Yes (~98% Cache Hit) Yes (Native `.m3u8` + `.m4s` support)
Edge Worker HMAC + Origin OAC CDN Edge PoP Yes (~99% Cache Hit) Yes (Supports prefix wildcard tokens)

Edge HMAC Token Verification + Cache-Key Stripping Pattern

To achieve both cryptographic access control and a 98%+ CDN cache-hit ratio, decouple **Client-to-Edge Authentication** from **Edge-to-Origin Authentication**:

  1. Client-to-Edge (Short-Lived HMAC Token): Your application signs the URL path and expiration timestamp (`?exp=1756740000&token=hmac_sha256`). The CDN Edge PoP verifies the HMAC signature in under 0.3ms.
  2. Cache Key Normalization: Immediately after verifying the signature, the Edge PoP **strips `exp` and `token` from the internal cache key**, looking up the clean URI (`https://cdn.daktan.com/releases/app-v2.dmg`) in the shared edge cache.
  3. Edge-to-Origin (Origin Access Control / R2 Binding): If the object is not in the edge cache, the CDN fetches it from the private origin bucket using its own static credential (CloudFront OAC or Cloudflare R2 Worker Binding).
// Production Cloudflare Worker: Edge HMAC Verification + Shared Cache Key Lookup
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const exp = parseInt(url.searchParams.get("exp") || "0", 10);
    const token = url.searchParams.get("token") || "";

    // 1. Reject expired requests immediately at the edge
    const nowSec = Math.floor(Date.now() / 1000);
    if (!exp || nowSec > exp) {
      return new Response("URL Expired", { status: 403 });
    }

    // 2. Verify HMAC-SHA256 signature over pathname + expiration
    const encoder = new TextEncoder();
    const key = await crypto.subtle.importKey(
      "raw", encoder.encode(env.EDGE_SIGNING_SECRET),
      { name: "HMAC", hash: "SHA-256" }, false, ["verify"]
    );
    const data = encoder.encode(`${url.pathname}:${exp}`);
    const sigBytes = Uint8Array.from(atob(token), c => c.charCodeAt(0));
    const isValid = await crypto.subtle.verify("HMAC", key, sigBytes, data);

    if (!isValid) {
      return new Response("Invalid Signature", { status: 403 });
    }

    // 3. Strip auth params so all authorized users hit the exact same shared cache key
    const cleanCacheUrl = new URL(url.origin + url.pathname);
    const cacheKey = new Request(cleanCacheUrl.toString(), request);
    const cache = caches.default;

    let response = await cache.match(cacheKey);
    if (!response) {
      const object = await env.PRIVATE_R2_BUCKET.get(url.pathname.slice(1));
      if (!object) return new Response("Not Found", { status: 404 });

      const headers = new Headers();
      object.writeHttpMetadata(headers);
      headers.set("Cache-Control", "public, s-maxage=604800");
      response = new Response(object.body, { headers });
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
    }
    return response;
  }
};

Verifying Shared Cache Hits Across Distinct User Tokens

To confirm that two different authenticated users with different `exp` timestamps and `token` hashes actually hit the same edge cache entry, generate two distinct tokens and inspect the `Age` header:

# User A Request (exp=1788290100) -> Populates Edge Cache (MISS)
$ curl -sI "https://cdn.daktan.com/courses/k8s-networking.mp4?exp=1788290100&token=a8F2mK9pL1..." | grep -iE "HTTP/|cache|age"
HTTP/2 200
cache-control: public, s-maxage=604800

# User B Request 12 Seconds Later (Different exp=1788290950 & Different HMAC Token) -> Served from Shared Cache (HIT!)
$ curl -sI "https://cdn.daktan.com/courses/k8s-networking.mp4?exp=1788290950&token=x9Q4vR2wZ8..." | grep -iE "HTTP/|cache|age"
HTTP/2 200
cache-control: public, s-maxage=604800
age: 12

Securing HLS / DASH Streaming with Path-Scoped Wildcard Tokens

When a video player loads an HLS `master.m3u8` playlist, it subsequently requests hundreds of relative child URLs (`1080p/chunk-0001.m4s`, `1080p/chunk-0002.m4s`). Because native iOS Safari `

  • Signed Cookies (`CloudFront-Policy` / Edge Cookie): Set an `HttpOnly; Secure; SameSite=Lax` cookie scoped to `Path=/videos/video_id_942/` containing the HMAC signature and expiration. Every subsequent segment request automatically includes the cookie without mutating URL cache keys.
  • Path-Prefix Token Embedding: Embed the token as a virtual path prefix (`/sig_1756740000_9a8f2c/videos/video_id_942/master.m3u8`). Because the HLS manifest uses relative paths (`chunk-0001.m4s`), the browser automatically prepends `/sig_1756740000_9a8f2c/videos/video_id_942/` to every child segment request, which the Edge Worker validates and strips before cache lookup.

Reduce Media Bandwidth by 55% with AVIF & WebP Next

Read our quantitative audit of image compression ratios vs on-the-fly edge transformation compute pricing.

Image Optimization Guide →