Context
From #7 point 3: now that we have a Lambda (sync endpoint), we can level up audio content protection. This issue captures comprehensive research on AWS streaming security options, cost modeling across deployment scales, and a recommended migration path.
Primary concern: Protecting the music.
Secondary concerns: Efficiency, resiliency, observability, cost management.
Constraint: Maintain backwards compatibility — existing deployments (e.g., 36247) can stay on current architecture.
Current Architecture (Tier 0)
| Component |
Details |
| Audio storage |
Private S3 bucket |
| CDN |
CloudFront with PriceClass_100 (US/CA/EU) |
| Auth |
Signed cookies (RSA-SHA1, canned policy) |
| Cookie TTL |
1 year (hardcoded in deploy-cookies.py and cookies.js) |
| Cookie delivery |
Baked into js/config.js at deploy time, precached by service worker |
| Seeking |
Byte-range requests supported (S3 CORS + CloudFront) |
| Compression |
Disabled for audio (correct — MP3 is already compressed) |
| DRM |
None |
| Observability |
GA page-level only — no CF access logs, no CloudWatch alerts |
Key Security Gaps
- 1-year static cookies — anyone who visits the site has year-long access to all audio files. Cookies are precached in the SW shell, so they persist even if the user clears browser cookies.
- Wildcard resource pattern — cookies grant access to
https://{domain}/*, not per-track.
- No cookie rotation — cookies never refresh until a full redeploy.
cookies.js hardcodes max-age=31536000 — ignores the server-side policy TTL entirely.
cookie_ttl_hours terraform var (default 24) is disconnected from deploy-cookies.py's --hours 8760 default.
Tier Options
Tier 1b: Cookie Refresh via Lambda (⭐ Recommended First Step)
What: Add a /sync/refresh route to the existing sync Lambda. Shorten cookie TTL from 1 year to 72 hours. Client auto-refreshes on app load.
Why this first: Zero additional AWS cost. Biggest security improvement per unit of effort. No new services. No client library changes. SW-compatible.
Changes needed:
| Area |
Change |
| Terraform |
Add secretsmanager:GetSecretValue to sync Lambda IAM role for the signing key secret |
| Lambda |
Add /sync/refresh route: fetch RSA key from Secrets Manager (cached in memory), generate fresh canned-policy cookies, return as JSON |
cookies.js |
Add getCookieExpiry() to parse CloudFront-Policy cookie. Add refreshCookiesIfNeeded() — call on app load, refresh if within 24h of expiry |
deploy-cookies.py |
Change default --hours from 8760 to 72 |
config.js |
Can revert to SIGNED_COOKIES = null — cookies come from Lambda, not baked config |
| SW |
Remove config.js dependency for cookies — stale-while-revalidate race condition eliminated |
Cookie refresh flow:
App loads → getCookieExpiry() → within 24h of expiry?
→ Yes: fetch("/sync/refresh") → set fresh cookies → proceed
→ No: cookies still valid → proceed
Risks:
samesite=strict may cause issues on iOS PWA launch (cross-context navigation). Test carefully.
- Existing deployments without the Lambda endpoint will fail to refresh. Ship the endpoint first, then shorten TTL.
Tier 1: Signed URLs via Lambda (Per-Track)
What: Generate a signed URL per track at play time via Lambda. Each URL expires in 15 minutes.
Pros: Per-track granularity, short-lived, auditable.
Cons: Breaks service worker audio caching (URL includes query params that change per generation). Requires either stripping query params in SW cache matching or accepting no SW caching.
Cost impact: Zero at small/medium scale (Lambda free tier). At large scale, Secrets Manager API calls dominate if key isn't cached in Lambda memory — mitigated by in-memory caching.
CloudFront supports signed cookies AND signed URLs simultaneously — no terraform changes needed. Both are validated by the same trusted_key_groups.
Tier 2: HLS + AES-128 Encryption
What: Transcode MP3 to HLS segments, encrypt with AES-128-CBC, serve key via authenticated Lambda endpoint.
Pros: Segments are ciphertext at rest. Casual copying requires finding the key server. Seeking improves (segment-level jumps).
Cons: Requires hls.js (~250KB) for Chrome/Firefox. Native <audio> doesn't support HLS except Safari. Key server Lambda needs its own auth. ffmpeg can still decrypt with the key.
| Component |
Small (100 tracks) |
Medium (500) |
Large (2000) |
| MediaConvert (one-time) |
|
|
|
| Additional S3 storage |
+/bin/zsh.07/mo |
+/bin/zsh.35/mo |
+.40/mo |
| Additional CF requests (60x/play) |
negligible |
/bin/zsh.25/mo |
.50/mo |
| Key server Lambda |
free tier |
free tier |
</mo |
Tier 3: Full DRM (MediaPackage + Widevine/FairPlay)
What: AWS Elemental MediaPackage with commercial DRM license server.
Cost: +/month minimum for the license server alone (BuyDRM, EZDRM, etc.). MediaPackage adds ~2x the per-GB cost vs CloudFront direct.
Verdict: Non-starter for personal/self-hosted use. Only viable at commercial streaming scale.
Cost Summary
| Tier |
Small (100T, 10U/day) |
Medium (500T, 100U/day) |
Large (2000T, 1000U/day) |
Key Tradeoff |
| 0: Current |
/bin/zsh.40*/mo |
/bin/zsh.40*/mo |
.56/mo |
1-year static cookies |
| 1b: Cookie refresh |
/bin/zsh.40*/mo |
/bin/zsh.40*/mo |
.56/mo |
72h TTL, auto-refresh |
| 1: Signed URLs |
/bin/zsh.40*/mo |
/bin/zsh.40*/mo |
.56/mo |
Per-play signing, breaks SW cache |
| 2: HLS+AES-128 |
/bin/zsh.50/mo + 1x |
/bin/zsh.75/mo + 1x |
.56/mo + 1x |
Needs hls.js + key server |
| 3: Full DRM |
+/mo |
+/mo |
+/mo |
Commercial only |
*Free tier covers entirely (1M Lambda reqs/mo + 1TB CF transfer/mo).
Observability (All Tiers)
Currently no CF access logs are enabled. Recommended additions:
- CloudFront access logs → S3 logging bucket → Athena queries (~20 lines terraform, /bin/zsh/mo at small scale)
- AWS Budgets alert at /mo threshold (~10 lines terraform)
- Lambda CloudWatch metrics already auto-publish (Invocations, Errors, Duration)
Key metrics for a music streaming service:
- Unique listeners/day (CF logs, IP approximation)
- Track play counts (CF logs, 200s on
/audio/*)
- Cache hit ratio (CF console)
- Data transfer/day
- Audio error rate (403/404 on
/audio/*)
Backwards Compatibility
CloudFront validates signed cookies first, then signed URLs. Both work simultaneously on the same distribution with the same trusted_key_groups. No terraform changes needed to support mixed deployments.
Migration path:
- Phase 0 (current): All instances on Tier 0.
- Phase 1: Crate adds cookie refresh. 36247 stays on Tier 0. Both work.
- Phase 2 (optional): Crate adds signed URL support. 36247 unchanged.
- Phase 3 (optional): Crate adds HLS+AES-128 for new uploads. 36247 unchanged.
Recommendation
Start with Tier 1b (cookie refresh). It's the highest-leverage change:
- Shrinks the attack window from 1 year to 72 hours
- Zero additional AWS cost
- Reuses the existing sync Lambda
- SW-compatible (no cache-breaking URL changes)
- 1 terraform change + 1 Lambda route + 1 client function
Then add observability (CF logs + budget alert) and defer HLS until there's a concrete need for stronger protection.
Relevant Files
| File |
Role |
terraform/cloudfront.tf |
Distribution config, trusted_key_groups on audio behavior |
terraform/cloudfront-keys.tf |
RSA-2048 key in Secrets Manager, key group |
terraform/lambda-sync.tf |
Sync Lambda, IAM role (needs SM access) |
terraform/variables.tf |
cookie_ttl_hours var (default 24, unused by deploy script) |
tools/deploy-cookies.py |
Generates signed cookies, bakes into config.js |
www/js/cookies.js |
Sets cookies, hardcodes 1-year max-age |
www/js/config.js |
SIGNED_COOKIES = null (replaced at deploy) |
www/sw.js |
Precaches config.js in shell, cache-first audio |
Acceptance Criteria
Research Notes
(populated by Phase 1 researcher)
Task DAG
Session State
(set on first deploy)
Dev Diary & Transcript
(appended on finish)
Context
From #7 point 3: now that we have a Lambda (sync endpoint), we can level up audio content protection. This issue captures comprehensive research on AWS streaming security options, cost modeling across deployment scales, and a recommended migration path.
Primary concern: Protecting the music.
Secondary concerns: Efficiency, resiliency, observability, cost management.
Constraint: Maintain backwards compatibility — existing deployments (e.g., 36247) can stay on current architecture.
Current Architecture (Tier 0)
PriceClass_100(US/CA/EU)deploy-cookies.pyandcookies.js)js/config.jsat deploy time, precached by service workerKey Security Gaps
https://{domain}/*, not per-track.cookies.jshardcodesmax-age=31536000— ignores the server-side policy TTL entirely.cookie_ttl_hoursterraform var (default 24) is disconnected fromdeploy-cookies.py's--hours 8760default.Tier Options
Tier 1b: Cookie Refresh via Lambda (⭐ Recommended First Step)
What: Add a
/sync/refreshroute to the existing sync Lambda. Shorten cookie TTL from 1 year to 72 hours. Client auto-refreshes on app load.Why this first: Zero additional AWS cost. Biggest security improvement per unit of effort. No new services. No client library changes. SW-compatible.
Changes needed:
secretsmanager:GetSecretValueto sync Lambda IAM role for the signing key secret/sync/refreshroute: fetch RSA key from Secrets Manager (cached in memory), generate fresh canned-policy cookies, return as JSONcookies.jsgetCookieExpiry()to parse CloudFront-Policy cookie. AddrefreshCookiesIfNeeded()— call on app load, refresh if within 24h of expirydeploy-cookies.py--hoursfrom 8760 to 72config.jsSIGNED_COOKIES = null— cookies come from Lambda, not baked configCookie refresh flow:
Risks:
samesite=strictmay cause issues on iOS PWA launch (cross-context navigation). Test carefully.Tier 1: Signed URLs via Lambda (Per-Track)
What: Generate a signed URL per track at play time via Lambda. Each URL expires in 15 minutes.
Pros: Per-track granularity, short-lived, auditable.
Cons: Breaks service worker audio caching (URL includes query params that change per generation). Requires either stripping query params in SW cache matching or accepting no SW caching.
Cost impact: Zero at small/medium scale (Lambda free tier). At large scale, Secrets Manager API calls dominate if key isn't cached in Lambda memory — mitigated by in-memory caching.
CloudFront supports signed cookies AND signed URLs simultaneously — no terraform changes needed. Both are validated by the same
trusted_key_groups.Tier 2: HLS + AES-128 Encryption
What: Transcode MP3 to HLS segments, encrypt with AES-128-CBC, serve key via authenticated Lambda endpoint.
Pros: Segments are ciphertext at rest. Casual copying requires finding the key server. Seeking improves (segment-level jumps).
Cons: Requires
hls.js(~250KB) for Chrome/Firefox. Native<audio>doesn't support HLS except Safari. Key server Lambda needs its own auth.ffmpegcan still decrypt with the key.Tier 3: Full DRM (MediaPackage + Widevine/FairPlay)
What: AWS Elemental MediaPackage with commercial DRM license server.
Cost: +/month minimum for the license server alone (BuyDRM, EZDRM, etc.). MediaPackage adds ~2x the per-GB cost vs CloudFront direct.
Verdict: Non-starter for personal/self-hosted use. Only viable at commercial streaming scale.
Cost Summary
*Free tier covers entirely (1M Lambda reqs/mo + 1TB CF transfer/mo).
Observability (All Tiers)
Currently no CF access logs are enabled. Recommended additions:
Key metrics for a music streaming service:
/audio/*)/audio/*)Backwards Compatibility
CloudFront validates signed cookies first, then signed URLs. Both work simultaneously on the same distribution with the same
trusted_key_groups. No terraform changes needed to support mixed deployments.Migration path:
Recommendation
Start with Tier 1b (cookie refresh). It's the highest-leverage change:
Then add observability (CF logs + budget alert) and defer HLS until there's a concrete need for stronger protection.
Relevant Files
terraform/cloudfront.tftrusted_key_groupson audio behaviorterraform/cloudfront-keys.tfterraform/lambda-sync.tfterraform/variables.tfcookie_ttl_hoursvar (default 24, unused by deploy script)tools/deploy-cookies.pywww/js/cookies.jswww/js/config.jsSIGNED_COOKIES = null(replaced at deploy)www/sw.jsAcceptance Criteria
Research Notes
(populated by Phase 1 researcher)
Task DAG
Session State
(set on first deploy)
Dev Diary & Transcript
(appended on finish)