Skip to main content
Encrypted HLS segments should be served from a CDN. The CDN cannot decrypt the content (zero-knowledge) — it caches and delivers opaque bytes, just like any other static asset.

Architecture

CORS headers

The player fetches segments via fetch() from a different origin than your app. The CDN must return CORS headers.

CloudFront

Create a response headers policy:
{
  "CorsConfig": {
    "AccessControlAllowOrigins": {
      "Items": ["https://app.example.com"]
    },
    "AccessControlAllowMethods": {
      "Items": ["GET", "HEAD"]
    },
    "AccessControlAllowHeaders": {
      "Items": ["*"]
    },
    "AccessControlExposeHeaders": {
      "Items": ["Content-Length", "Content-Range"]
    },
    "OriginOverride": true
  }
}
Attach this policy to your CloudFront distribution’s behavior for the segment path pattern (/content/*).

Cloudflare

Add a Transform Rule or Worker to set CORS headers:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, HEAD
Access-Control-Expose-Headers: Content-Length, Content-Range
Or use Cloudflare’s CORS settings in the dashboard under your zone’s security settings.

Cache policy

AssetCache behaviorWhy
.ts segmentsCache forever (max-age=31536000, immutable)Segments are content-addressed and never change
.m3u8 manifestsShort TTL or no-cache (max-age=0, must-revalidate)Manifests may be updated (e.g., live streams, key rotation)
.key filesDo not cacheKey requests go directly to the key server, not the CDN

CloudFront cache policy

Create a cache policy for segments:
{
  "MinTTL": 86400,
  "MaxTTL": 31536000,
  "DefaultTTL": 31536000
}
For manifests, use a separate behavior with MinTTL: 0 and forward the Cache-Control header from the origin.

S3 metadata

Set cache headers when uploading segments:
# The CLI sets these automatically during upload
# If uploading manually:
aws s3 cp seg-0.ts s3://bucket/content/vid-001/seg-0.ts \
  --cache-control "max-age=31536000, immutable" \
  --content-type "video/mp2t"

S3 as origin

CloudFront + S3

  1. Create an S3 bucket (private, no public access)
  2. Create a CloudFront distribution with the S3 bucket as origin
  3. Use Origin Access Control (OAC) to let CloudFront read from S3 without making the bucket public
  4. Set the origin path to match your upload prefix (e.g., /content)

Cloudflare + R2

R2 buckets can be connected as custom domains in Cloudflare:
  1. Create an R2 bucket
  2. Connect a custom domain (e.g., cdn.example.com)
  3. R2 serves objects directly with Cloudflare’s CDN

Security

The CDN sees only encrypted bytes. Even if CDN cache is compromised, the attacker gets ciphertext without the decryption key. The key server is a separate service that requires authentication. Do not cache key server responses. Key requests contain authentication tokens and return cryptographic material. They should always go directly to the key server.

Testing

Verify CORS headers are set correctly:
curl -I -H "Origin: https://app.example.com" \
  https://cdn.example.com/content/vid-001/seg-0.ts

# Should include:
# Access-Control-Allow-Origin: https://app.example.com
Verify cache behavior:
# First request: cache miss
curl -I https://cdn.example.com/content/vid-001/seg-0.ts
# X-Cache: Miss from cloudfront (or CF-Cache-Status: MISS)

# Second request: cache hit
curl -I https://cdn.example.com/content/vid-001/seg-0.ts
# X-Cache: Hit from cloudfront (or CF-Cache-Status: HIT)