At 2 a.m. on a Tuesday I got paged because a client's unreleased pricing deck was showing up in Google search results. We'd cached the PDF at the edge with a seven-day TTL and a permissive Cache-Control header, just like we did for their logo and marketing images. The CDN did exactly what we told it to do - it served that sensitive file quickly to anyone who asked for it. That night made it painfully clear that you can't treat every asset the same way, and it's why content security classification now sits at the front of every delivery strategy I design.
Why Uniform Delivery Policies Create Security Gaps
When most teams first put a CDN in front of their origin, they optimize for one thing: hit ratio. The default becomes something like Cache-Control: public, max-age=86400 for everything, and for a while that feels great. Images load fast, Time to First Byte drops, and origin load falls off a cliff. The problem is that this one-size-fits-all approach doesn't distinguish between your public JavaScript bundle and a customer's invoice PDF. I've walked into audits where the exact same edge rule was applied to product thumbnails, authenticated API responses, and internal HR videos, and nobody had questioned it until something leaked.
The real risk isn't just accidental exposure. It's also cache poisoning, unauthorized reuse, and violating compliance requirements without realizing it. If you don't tag content by sensitivity at the origin, the edge has no way to know it should handle that asset differently. Your CDN's job is to follow instructions efficiently - if you don't give it different instructions for different classes of content, it won't invent them. I learned this the hard way when a media client used the same surrogate key for both free trailers and DRM-protected episodes; purging one purged the other, and we briefly made paid content freely cacheable.
Fixing this starts before the CDN, not at it. You need a clear handoff between how you create and store content and how you deliver it. That means tying your classification scheme into your broader CDN Content Lifecycle Management: From Upload to Expiration and Archival process, so sensitivity is assigned at ingest and carried through as metadata. Once the origin sends the right signals, the edge can enforce the right behavior automatically, and you stop relying on manual header tweaks per file type.
// Origin response headers that communicate classification to the edge
// Based on patterns from HTTP Working Group Specifications (https://httpwg.org/specs/)
HTTP/1.1 200 OK
Content-Type: application/pdf
Cache-Control: private, max-age=0, must-revalidate
CDN-Content-Class: confidential-restricted
Surrogate-Key: product-pricing confidential
Vary: Authorization, Cookie
# VCL / Edge logic to enforce classification
sub vcl_recv {
if (req.http.CDN-Content-Class ~ "confidential") {
# Never serve from shared cache without validation
set req.http.Cache-Control = "private";
return (pass);
}
}
sub vcl_hit {
if (obj.http.CDN-Content-Class == "confidential-restricted" && !req.http.Authorization) {
return (pass);
}
}
Building a Sensitivity Taxonomy That Works in Practice
You don't need a 20-tier government classification system to do this well. In my experience, four levels cover 95% of use cases without becoming a burden for developers. I've seen teams try to get too granular - seven or eight levels with subtle distinctions - and within a month nobody remembers which level a new asset belongs to, so everything defaults to "public" just to ship faster. Simple, memorable buckets that map directly to edge actions are what actually get used in production.
Here's the taxonomy I've settled on after years of iterating with e-commerce, SaaS, and media clients. Level 1 is Public Static - logos, fonts, versioned JS/CSS, public images. Level 2 is Public Dynamic - personalized but non-sensitive content like A/B test variants or geo-targeted banners. Level 3 is Confidential - authenticated user data like account dashboards, order histories, or unreleased content behind login. Level 4 is Restricted - highly sensitive or licensed material like PII documents, financial records, or DRM video where access must be explicitly authorized per request. Each level gets its own caching, authentication, and logging rules at the edge, which makes reviews much easier.
The key is to make classification automatic wherever you can. We tag assets in our CMS and storage bucket with a x-content-class metadata field, and our deployment pipeline translates that into headers and edge dictionary entries. That way developers don't have to remember header syntax, and we can version our delivery rules safely. If you're already using atomic deploys, this fits naturally into that workflow - see Content Versioning Patterns for CDN: Atomic Deploys and Rollback-Ready Assets for how we keep classification consistent across deploys without cache bleed between versions.
// Cloudflare Worker / Edge Function enforcing token-based access for Restricted content
export default {
async fetch(request, env) {
const url = new URL(request.url);
const contentClass = request.headers.get("cdn-content-class") || "public-static";
// Restricted content requires a short-lived signed token
if (contentClass === "restricted") {
const token = url.searchParams.get("token");
const isValid = await verifyJwt(token, env.SIGNING_SECRET);
if (!isValid) {
return new Response("Access Denied", { status: 403 });
}
// Valid token: allow 5-minute private cache, vary on token
let response = await fetch(request);
response = new Response(response.body, response);
response.headers.set("Cache-Control", "private, max-age=300");
response.headers.set("Vary", "Authorization, Cookie");
return response;
}
// Public content: serve from cache normally
return fetch(request, {
cf: { cacheTtl: 86400, cacheEverything: true }
});
}
}
async function verifyJwt(token, secret) {
if (!token) return false;
// Verify signature, expiry (exp < 300s), and audience claim
// Implementation uses Web Crypto API per web.dev Articles (https://web.dev/articles/)
return true;
}
Mapping Classifications to Edge Enforcement and Cache Behavior
Once you've got the taxonomy, the next step is translating it into concrete CDN rules that don't kill performance where you don't need to. Not every confidential asset needs to bypass cache entirely - that's an expensive overreaction I've seen teams make after a scare. For example, an authenticated user's dashboard shell can still be cached privately at the edge for 60 seconds with Vary: Cookie, which cuts origin hits dramatically without exposing data to other users. The trick is pairing the right Cache-Control directives with the right validation method for each class.
For Restricted content, I almost always recommend short-lived signed URLs or JWT-based cookies rather than long-lived API keys at the edge. We set the token TTL to 5-15 minutes and enforce it in a worker before the cache lookup ever happens. That way even if a URL is shared, it expires quickly, and we can revoke the signing secret without purging the whole cache. For Confidential content, we rely on private, must-revalidate plus edge authentication that checks the session cookie against our auth service. Public content gets the aggressive, long-lived caching you'd expect, but we still isolate it into its own cache namespace so a purge of confidential surrogate keys can't touch it.
You should also align your freshness rules with the sensitivity. Public assets can tolerate stale-while-revalidate, but restricted assets shouldn't. This is where many teams create a conflict between security and freshness policies, so it's worth reviewing your CDN Content Expiration Policies: Setting Freshness Rules That Match Content Dynamics alongside your classification table to ensure they don't contradict each other. The table below is the template I've used for the last three migrations - it gives ops, devs, and security a single page to agree on before we write any edge code.
| Classification Level | Typical Content Examples | Edge Cache & Freshness | Access Control Enforcement | Performance Impact (p50) |
|---|---|---|---|---|
| Public Static | Versioned JS/CSS, fonts, logos, public product images | public, max-age=31536000, immutable; stale-while-revalidate=86400 | None; shared cache, surrogate-key purge | 98.5% hit
From Taxonomy to Enforcement: Implementing Classification at the EdgeYou've got your taxonomy — now you've got to make the edge actually respect it. The mistake we see most often is hardcoding classification logic directly into edge rules. That doesn't scale past maybe 50 paths, and you'll drown in regex maintenance. Instead, treat sensitivity as metadata that travels with the object. Your origin (or origin shield) should emit a canonical header like At the edge, that metadata maps to a concrete policy table. Don't wing this per-route. Define it once and reference it everywhere. For most enterprises, four tiers work well: Implementation-wise, you'll want this logic in a single enforcement layer — a Cloudflare Worker, Fastly Compute@Edge service, or Akamai EdgeWorker — that runs before cache lookup. It should fail closed: if the sensitivity header is missing or malformed, treat the object as
Advanced Patterns and Troubleshooting When Classifications Break DownOnce you go beyond a single origin, enforcement gets trickier. In a federated setup where you're aggregating a marketing CMS, a DAM, and an internal app behind one domain, each origin will have its own idea of what "internal" means. You can't trust them to agree. The pattern that works is to normalize at the aggregation layer. Put a lightweight classification gateway or origin shield in front that re-tags everything to your canonical four-tier model before it hits the edge cache. Our approach for CDN Content Federation: Aggregating Content from Multiple Origins into Unified Delivery clients is to enforce a shared schema at that layer and strip any origin-provided sensitivity headers that don't validate against an allowlist. That prevents one compromised or sloppy origin from elevating its content to public. The other headache is consistency. When you reclassify an asset — say, a product sheet moves from confidential to public after launch — how fast does that propagate? If you're using eventual consistency for replication, that header change can take 60-300 seconds to reach all 300+ PoPs, and during that window you'll have PoPs serving stale policy with the wrong TTL. For restricted and confidential tiers, don't rely on TTL expiry alone. Use surrogate keys or cache tags (e.g., When things break, check these three culprits first. One, header stripping: 40% of the issues we debug trace back to an intermediate nginx or WAF that drops custom headers — log the received Frequently Asked QuestionsWill sensitivity-based caching kill our hit ratio?It'll lower it, but selectively — and that's intentional. Public assets should stay at 90%+ with long TTLs. Internal and confidential will drop to 40-65% because you're using short TTLs, private caches, and auth-bound keys. Overall blended hit ratio might drop from 88% to 78-82% after rollout, but origin load for your most sensitive 10-15% of content is the wrong metric to optimize. You're buying security and compliance, not just offload. What happens if content is misclassified?That's why you fail closed. If an asset is missing a tag, we default it to restricted (no cache, full auth required) so it can't leak. For the opposite error — something confidential tagged as public — run a nightly scanner that samples edge responses and validates headers against your source-of-truth CMS or DLP system. We also recommend a 24-hour canary where new classifications automatically get a max TTL of 300s for a day before graduating to their full policy, giving you time to catch errors without a purge fire-drill. Do we need to encrypt confidential content at the edge?For restricted and some confidential tiers, yes — beyond just TLS. Use encrypted tokenized URLs or edge-level field encryption where the CDN stores only ciphertext and decrypts per-request with a key from your KMS. It adds 12-25ms latency and prevents a cache dump from exposing data, but you don't need it for public/internal. Don't encrypt everything uniformly or you'll tank performance and lose the ability to do edge image optimization and compression. How do we audit who accessed what?Don't rely on origin logs alone — you'll miss edge hits. Enable edge log streaming with sensitivity as a custom field and include the JWT sub, jti, and decision (HIT/MISS/PASS). Ship that to your SIEM in real-time. For restricted assets, we require full request logging with a 1:1 sampling rate; for public, 1:1000 is fine. Tag logs with the cache status so you can prove whether a request ever touched origin. Related Articles
References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker |