CDN Content Security Classification: Applying Access Controls Based on Sensitivity

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 Edge

You'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 X-Content-Sensitivity: confidential and a secondary X-Content-Owner: finance-team, and the edge should enforce based on that — not on URL patterns. If you've already invested in a CDN Content Taxonomy: Organizing Assets for Efficient Edge Delivery and Invalidation strategy, you're halfway there; just extend that taxonomy's metadata to include the sensitivity tier.

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: public (cache 86400s, no auth, serve stale up to 24h), internal (cache 3600s, requires valid JWT with employee claim, no stale-while-revalidate), confidential (cache 60s private with surrogate-key, requires mTLS + short-lived token with 120s expiry, Cache-Control: private, no-store at client), and restricted (no edge cache at all, bypass_cache = true, every request goes to origin with full audit logging). This gives you predictable performance — we typically see public assets hold a 92-95% edge hit ratio while confidential stays around 45-60%, which is expected. You’re trading hit ratio for control, and that's the point.

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 restricted and deny. We've seen incidents where a misconfigured CMS stripped headers and suddenly confidential PDFs were served as public with a 1-day TTL. A default-deny saves you. It also adds only 8-15ms of compute latency at p95 if you keep your policy table in Edge KV or a dictionary, not fetched over the network.

// Cloudflare Worker - sensitivity-aware enforcement (before cache)
const POLICY = {
  public:       { ttl: 86400, auth: false, cache: 'public, max-age=86400' },
  internal:     { ttl: 3600,  auth: 'jwt',  cache: 'private, max-age=3600' },
  confidential: { ttl: 60,    auth: 'jwt_strict', cache: 'private, no-store' },
  restricted:   { ttl: 0,     auth: 'mtls', cache: 'no-store, private' }
};

export default {
  async fetch(req, env, ctx) {
    const sensitivity = req.headers.get('X-Content-Sensitivity') || 'restricted';
    const policy = POLICY[sensitivity] || POLICY['restricted'];

    // Fail closed + validate auth before touching cache
    if (policy.auth === 'jwt_strict' && !await isValidJWT(req, { maxAge: 120 })) {
      return new Response('Forbidden', { status: 403 });
    }
    if (policy.auth === 'jwt' && !await isValidJWT(req, { maxAge: 900 })) {
      return new Response('Unauthorized', { status: 401 });
    }

    // Apply tiered caching: never cache restricted, short TTL for confidential
    if (policy.ttl === 0) {
      return fetch(req, { cf: { cacheTtl: 0, cacheEverything: false } });
    }
    const res = await fetch(req, { cf: { cacheTtl: policy.ttl } });
    res.headers.set('Cache-Control', policy.cache);
    res.headers.set('Vary', 'Authorization, X-Content-Sensitivity');
    return res;
  }
}

Advanced Patterns and Troubleshooting When Classifications Break Down

Once 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., surrogate-key: sensitivity-confidential doc-id-4821) and issue a purge-by-key on reclassification. It’s effectively a strong consistency action for that small subset. As we detail in CDN Content Replication and Consistency Models: Eventual, Strong, and Causal, you'll want eventual for public assets where a 5-minute delay is fine, but treat policy changes for the top two tiers as strong-consistency events.

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 X-Content-Sensitivity at the edge and alert if it’s missing for more than 0.5% of requests. Two, cache key collisions: if you don't include Vary: Authorization or a token hash in the cache key for internal/confidential, the first authenticated user will poison the cache for unauthenticated users — or worse, vice versa. Three, token replay: if your confidential TTL is 60s but your JWT lives for 30 minutes, a revoked user still has a valid cached copy. Bind cache keys to a token jti or keep confidential edge TTL shorter than your revocation window. If hit ratio for confidential suddenly spikes above 75%, you’ve probably got a keying bug, not a performance win.

Frequently Asked Questions

Will 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