CDN Trust Mapping & Routing

Permalink to "CDN Trust Mapping & Routing"

This workflow is part of Asset Hashing & Dynamic Script Injection. Without explicit trust boundaries between your CDN origins and the assets they serve, a compromised edge node, a misconfigured origin, or a supply chain substitution can silently replace a JavaScript bundle with malicious bytes — and no CSP header alone will catch it before the browser executes the payload.

The workflow on this page binds cryptographic hashes to routing rules so every CDN origin is either explicitly trusted (and carries a verified hash manifest) or blocked before the response reaches the user. It covers multi-CDN tiering, CI/CD manifest gating, configuration reference, troubleshooting, and monitoring.

Prerequisites

Permalink to "Prerequisites"

Conceptual Foundation: How Origin Trust Tiers Work

Permalink to "Conceptual Foundation: How Origin Trust Tiers Work"

The W3C SRI specification (§3.3) delegates all integrity enforcement to the browser: the browser fetches the resource, decodes any Content-Encoding compression, computes the digest of the decoded bytes, and compares it against the integrity attribute. The CDN’s sole obligation is to deliver bytes unmodified.

That gives rise to a clean separation of responsibilities:

  • Browser — verifies that the bytes it received match the hash in the DOM.
  • CDN — routes traffic and caches responses without mutating the decoded payload.
  • Origin CI pipeline — generates hashes and publishes the authoritative manifest.
  • Routing rules — enforce which origins are permitted to serve a given path.

Multi-CDN architectures expand the attack surface because a secondary CDN that is not in the routing allow-list can still be DNS-reachable. Classifying origins into trust tiers and coupling those tiers to routing policy closes that gap.

Tier Origin type Verification strictness
Internal Your own cloud storage (S3, GCS, R2) Hash manifest required; CDN transformation disabled
Partner Known third-party CDN under contract (Fastly, Akamai) Hash manifest required; Content-Security-Policy: require-sri-for script style enforced
Public Open registries (unpkg, jsDelivr, cdnjs) Hash manifest required; integrity attribute mandatory; no fallback without hash

The diagram below illustrates how a build pipeline’s hash manifest flows through origin tiers to browser validation.

CDN Trust Mapping Data Flow A flow diagram showing the path from a CI build pipeline producing a hash manifest, through internal and partner CDN origin tiers, to the browser performing SRI hash verification. An untrusted public origin is blocked at the routing layer. CI Build Hash manifest Routing Origin allow-list Internal origin S3 / GCS / R2 Partner CDN Fastly / Akamai Public CDN Blocked — no manifest Browser SRI hash verify (decoded bytes) Tier 1 — Internal Tier 2 — Partner Tier 3 — Blocked

Step 1 — Classify Your CDN Origins and Assign Trust Tiers

Permalink to "Step 1 — Classify Your CDN Origins and Assign Trust Tiers"

Before writing a line of routing configuration, list every origin that serves assets referenced by your HTML. For each origin, record: the domain, whether your team controls it, whether the CDN provider has a signed SLA, and whether you can disable CDN-side payload transformation.

# Audit script: extract all unique script/link src domains from a built HTML directory
grep -rEoh 'src="https?://([^/"]+)' dist/ | \
  sed 's/src="https\?:\/\///' | sort -u > cdn-origins.txt
cat cdn-origins.txt

Expected output — a deduplicated list of hostnames, e.g.:

assets.example.com
cdn.jsdelivr.net
static.cloudflareinsights.com
unpkg.com

Assign each hostname to a tier (internal / partner / public) and record the result in cdn-trust-manifest.json alongside your build artifacts. This file becomes the authoritative source for the routing rule generator in Step 3.


Step 2 — Generate a Verified Hash Manifest at Build Time

Permalink to "Step 2 — Generate a Verified Hash Manifest at Build Time"

The hash manifest must be produced before any file reaches a CDN. Use Static Asset Hash Generation workflows to automate this. The manifest records the SHA-384 digest for every artifact and maps it to the CDN path it will occupy.

// scripts/generate-sri-manifest.js
// Run: node scripts/generate-sri-manifest.js --out dist/sri-manifest.json
import { createHash } from 'node:crypto';
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';

const DIST = new URL('../dist', import.meta.url).pathname;
const OUT  = process.argv[process.argv.indexOf('--out') + 1];

function hashFile(filePath) {
  const bytes = readFileSync(filePath);
  return 'sha384-' + createHash('sha384').update(bytes).digest('base64');
}

function walk(dir) {
  return readdirSync(dir).flatMap(name => {
    const full = join(dir, name);
    return statSync(full).isDirectory() ? walk(full) : [full];
  });
}

const manifest = {};
for (const file of walk(DIST)) {
  if (/\.(js|css|woff2|svg)$/.test(file)) {
    const key = '/' + relative(DIST, file);
    manifest[key] = hashFile(file);
  }
}

writeFileSync(OUT, JSON.stringify(manifest, null, 2));
console.log(`Manifest written: ${Object.keys(manifest).length} assets → ${OUT}`);

Verification signal — the script exits with code 0 and prints a count. Wire it into your CI stage:

# .github/workflows/deploy.yml (excerpt)
- name: Generate SRI manifest
  run: node scripts/generate-sri-manifest.js --out dist/sri-manifest.json

- name: Fail if manifest is empty
  run: |
    COUNT=$(jq 'length' dist/sri-manifest.json)
    if [ "$COUNT" -lt 1 ]; then echo "Empty manifest — aborting"; exit 1; fi
    echo "Manifest contains $COUNT assets"

Step 3 — Publish the Manifest to CDN Routing Configuration

Permalink to "Step 3 — Publish the Manifest to CDN Routing Configuration"

Push the manifest hashes into your CDN’s edge configuration. The exact API differs by provider; the example below targets the Cloudflare Workers KV store that a Worker reads at request time to enforce origin routing.

// scripts/sync-cdn-routing.js
// Run: node scripts/sync-cdn-routing.js --manifest dist/sri-manifest.json
import { readFileSync } from 'node:fs';

const manifest = JSON.parse(
  readFileSync(process.argv[process.argv.indexOf('--manifest') + 1], 'utf8')
);

const CF_ACCOUNT = process.env.CF_ACCOUNT_ID;
const CF_NS      = process.env.CF_KV_NAMESPACE_ID;
const CF_TOKEN   = process.env.CF_API_TOKEN;

const body = JSON.stringify(
  Object.entries(manifest).map(([key, hash]) => ({
    key,
    value: JSON.stringify({ hash, tier: 'internal', allowedAt: new Date().toISOString() }),
  }))
);

const res = await fetch(
  `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT}/storage/kv/namespaces/${CF_NS}/bulk`,
  {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${CF_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body,
  }
);

if (!res.ok) {
  const err = await res.text();
  console.error('CDN routing sync failed:', err);
  process.exit(1);
}
console.log(`Synced ${manifest.length ?? Object.keys(manifest).length} routing entries to KV`);

For Fastly, replace the fetch call with a Fastly NGWAF ACL update or a VCL snippet deploy via the Fastly Config API. For Akamai, use the Property Manager API to update the match condition on the origin routing rule.


Step 4 — Set integrity Attributes and CSP Headers on Origin Responses

Permalink to "Step 4 — Set integrity Attributes and CSP Headers on Origin Responses"

With the manifest in place, stamp every HTML <script> and <link> tag with the matching hash. If you use Automating Hash Generation in Webpack 5, the webpack-subresource-integrity plugin handles this automatically. For server-side rendering or static site generators, read the manifest at build time:

<!-- Correct: integrity + crossorigin both present -->
<script
  src="https://assets.example.com/app.fca3b9.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux8JHrklT1RNnZ3d8l6+pNnS3lPmW2"
  crossorigin="anonymous"
></script>

The crossorigin="anonymous" attribute is not optional. Without it, the browser sends credentials in the request and the CORS preflight is skipped — but SRI requires a CORS-eligible response for cross-origin resources. Omitting crossorigin causes a silent fetch-then-block failure that is extremely difficult to diagnose.

Pair the tag with a Configuring Content-Security-Policy with SRI header:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://assets.example.com 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux8JHrklT1RNnZ3d8l6+pNnS3lPmW2';
  require-sri-for script;
  report-to sri-violations

require-sri-for script instructs compliant browsers to refuse any <script> that lacks an integrity attribute, providing defence-in-depth against dynamically injected tags that the manifest does not cover. For runtime-injected scripts, consult Dynamic Script Loading Patterns.


Step 5 — Gate the CI/CD Pipeline on Mismatch Rate

Permalink to "Step 5 — Gate the CI/CD Pipeline on Mismatch Rate"

A deployment gate that reads live CSP violation reports prevents a broken manifest from propagating to production. The gate queries your reporting endpoint for the candidate bundle, computes the mismatch rate over the last N minutes, and blocks promotion if the rate exceeds a threshold.

#!/usr/bin/env bash
# scripts/check-sri-violation-rate.sh
# Usage: ./check-sri-violation-rate.sh <bundle-version> <threshold-percent>
set -euo pipefail

VERSION="${1:?Usage: check-sri-violation-rate.sh <version> <threshold>}"
THRESHOLD="${2:-1}"  # default: block if >1% of loads fail

VIOLATIONS=$(curl -sf "${REPORT_ENDPOINT}/violations?version=${VERSION}&minutes=10" | jq '.count')
LOADS=$(curl -sf "${REPORT_ENDPOINT}/loads?version=${VERSION}&minutes=10" | jq '.count')

if [ "$LOADS" -eq 0 ]; then
  echo "No load data yet — skipping gate (first deploy or pre-prod)"
  exit 0
fi

RATE=$(echo "scale=2; $VIOLATIONS * 100 / $LOADS" | bc)
echo "SRI mismatch rate: ${RATE}% (threshold: ${THRESHOLD}%)"

if (( $(echo "$RATE > $THRESHOLD" | bc -l) )); then
  echo "FAIL: mismatch rate exceeds threshold — blocking promotion"
  exit 1
fi

echo "PASS: mismatch rate within threshold"

Integrate this gate into the promote stage, not the build stage — run it after canary traffic has been routed to the new bundle for 10–15 minutes.


Step 6 — Monitor, Alert, and Roll Back on Anomalies

Permalink to "Step 6 — Monitor, Alert, and Roll Back on Anomalies"

Real-time telemetry from report-to violation endpoints is the primary signal for origin compromise. Configure your observability stack to alert when:

  • The per-asset mismatch rate exceeds 0.5% over a 5-minute rolling window
  • A new violated-directive: script-src event references a path that is not in the current manifest
  • Violation events correlate with a third-party CDN origin update (check CDN changelog webhooks)

Rollback procedure when mismatch rates spike:

  1. Identify the affected asset path from the violation report’s blocked-uri field.
  2. Check the CDN KV store — confirm the stored hash matches the build manifest. Divergence indicates CDN-side mutation or a race condition from a concurrent deployment.
  3. Purge the CDN cache for the affected path and re-serve from the internal origin.
  4. Re-deploy the previous verified bundle if purge does not resolve the mismatch.
  5. Post-incident: audit CDN transformation rules for the affected path; verify CORS headers are present and the Content-Encoding was not altered mid-flight.

For browser-enforced failure handling, see Handling SRI Failures with onerror Handlers.


Configuration Reference

Permalink to "Configuration Reference"
Option Valid values Security implication
integrity algorithm sha256-, sha384-, sha512- SHA-384 is the industry default; SHA-512 adds marginal security at higher payload cost; SHA-256 is insufficient for new deployments
crossorigin anonymous, use-credentials Must be anonymous for public CDN assets; use-credentials leaks session cookies in the SRI fetch
require-sri-for script, style, script style Apply to both to prevent stylesheet injection; script alone is the minimum for PCI DSS 6.4.3 compliance
CDN transformation disabled / edge-side includes off Any payload-mutating transformation invalidates the hash; disable for all SRI-protected paths
Cache purge coupling Tied to manifest publish step Purge must run after the new manifest is live — purging before causes a window where browsers receive new bytes with old hashes
Fallback origin Same content-hashed path Fallback origins must serve byte-identical content; never route to an origin that re-minifies or re-bundles at serve time

Integration with Adjacent Tooling

Permalink to "Integration with Adjacent Tooling"

CDN trust mapping sits between the hash generation step and the browser’s enforcement layer. Its output feeds forward into two adjacent workflows:

  • Mapping CDN Origins to SRI Policies — the detailed reference page that documents how to author the per-origin policy matrix, including how to map a CDN edge PoP list to a Content-Security-Policy script-src allow-list.
  • Browser Enforcement & Security Boundaries — once the CDN delivers bytes, this page documents exactly what the browser does: the fetch lifecycle, the hash computation algorithm, and every failure mode including opaque-response blocking.

For SBOM traceability — recording which CDN origin version served which bundle hash and when — combine this workflow with Automated SBOM Generation, which produces CycloneDX artifacts that reference SRI hashes as component evidence.


Troubleshooting

Permalink to "Troubleshooting"

net::ERR_SRI_FAILED — resource blocked in DevTools

The hash in the integrity attribute does not match the decoded bytes received. Most common cause: CDN-side minification or HTML rewriting is enabled for the path. Disable all payload-transforming rules for the affected path and hard-reload. Confirm by downloading the raw URL with curl --compressed and computing openssl dgst -sha384 -binary <file> | openssl base64 -A.

Refused to load the script ... it requires a valid nonce or hash to be present in the source list

The CSP require-sri-for script directive is active but the <script> tag lacks an integrity attribute. This occurs when a dynamic script injection path (analytics, A/B testing, tag managers) creates a tag without reading from the manifest. Apply the pattern from Implementing Dynamic Script Loaders with Integrity.

Hash mismatch only on some users / some geographies

A CDN PoP is serving a cached version of the asset that was built before the current manifest. The classic symptom is hash mismatch for ~5–15% of users immediately after a deployment. Fix: ensure the CDN cache purge runs atomically with the manifest sync in Step 3. Use content-addressed filenames (hash in the URL) to make this problem structurally impossible.

crossorigin attribute absent — request blocked with opaque response

The browser made a no-CORS fetch for a cross-origin resource that has an integrity attribute. Per the Fetch spec, SRI cannot validate opaque responses. Add crossorigin="anonymous" to the element and confirm the server responds with Access-Control-Allow-Origin: * or the origin’s specific value.

CI gate always passes even when CDN is misconfigured

The violation rate gate in Step 5 fires only after traffic reaches the new bundle. If the gate runs during the build stage (before any users see the asset) it will always read zero violations. Move the gate to the promote/swap step that runs after canary traffic has accumulated for at least 10 minutes.

Manifest published but KV store returns stale data

Cloudflare KV has eventual consistency with up to 60 seconds of propagation delay globally. If your CDN Worker reads the manifest on every request and you purge the cache simultaneously with the KV write, some PoPs may still read the old value for up to a minute. Mitigate by writing the new manifest to KV at least 90 seconds before the cache purge fires.


Security Boundary

Permalink to "Security Boundary"

CDN trust mapping enforces that bytes delivered by a CDN origin match the hash computed at build time. It does not:

  • Verify the integrity of the build process itself — a compromised CI pipeline can produce a malicious bundle with a valid hash. For build provenance, use Provenance Verification Workflows.
  • Protect against compromised npm packages that entered the build legitimately — SRI hashes the output bundle, not its source dependencies. Dependency-level verification requires Dependency Pinning Best Practices.
  • Guard against DOM-based injection after the script loads — once the browser has verified and executed a script, SRI’s job is done. Trusted Types and CSP script-src restrict runtime injection.
  • Prevent CDN metadata manipulation — DNSSEC records or TLS certificate substitution attacks require HTTPS certificate transparency monitoring, not SRI.

Permalink to "Related"

Articles in This Topic

Mapping CDN Origins to SRI Policies
Configuring SRI for jsDelivr and unpkg
Back to Asset Hashing & Dynamic Script Injection