# Edge protection and security headers

Verified 2026-09 against
<https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/dispatcher/cdn-configuring-traffic>
and <https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/security/home>.

## Traffic filter rules

Traffic filter rules live in `cdn.yaml` in the config pipeline and are evaluated at the CDN, before
the request reaches dispatcher or publish. They cover rate limiting, geography, IP lists and request
properties. WAF rules (separately licensed) add signature-based detection on top.

Working order:

1. Write the rule with the **log-only** action.
2. Deploy, then read what it *would* have blocked over a representative period — including a
   weekday peak and any batch integration window.
3. Fix the false positives. The first things a naive rate limit catches are your own integrations,
   a partner's crawler and the monitoring probe.
4. Switch to block, and keep watching.

Things worth rules: a request rate ceiling per client for expensive paths, geo restrictions where
the business genuinely has none of that market, an allow-list for admin-ish paths, and blocking
known-bad paths that only scanners request.

Things not worth rules: anything the dispatcher filter already denies by default, and anything you
cannot describe the false-positive case for.

## Purge keys

Cache invalidation is authorised by a purge key.

- Treat it as a secret: Cloud Manager secret variable, never in Git, never in client-side code.
- Rotate it, and know who owns the rotation. An unrotatable key shared across teams becomes a
  standing risk that nobody can remediate quickly.
- A leaked purge key is a denial-of-service: repeated purges push all traffic to origin.

## Security header set

Set these once at the edge for every response rather than per page:

| Header | Value to start from | Why |
|---|---|---|
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Stops protocol downgrade; add `preload` only when you are sure about every subdomain |
| `X-Content-Type-Options` | `nosniff` | Prevents MIME sniffing turning an upload into script |
| `Content-Security-Policy` | see below | The only header that meaningfully limits XSS impact |
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Stops paths and query strings leaking to third parties |
| `X-Frame-Options` / CSP `frame-ancestors` | Deny, or an explicit allow-list | Clickjacking. Note the author environment needs framing for the editor |
| `Permissions-Policy` | Deny the features the site does not use | Reduces what injected script can reach |

## CSP with client libraries

CSP is where AEM projects stall, because client libraries and the page editor both produce inline
script and style.

A rollout that works:

1. Deploy in **report-only** mode with a reporting endpoint, and leave it long enough to see real
   traffic, including the author environment and any third-party tag manager.
2. Read the reports. Expect inline handlers in older components, inline style attributes, and
   analytics or personalisation vendors loading further scripts.
3. Remove what you can: move inline script into a clientlib file, replace inline `onclick` with
   listeners, move inline style into CSS.
4. For what genuinely must stay inline, use a nonce, regenerated per response — not
   `unsafe-inline`, which removes most of the protection.
5. Keep a separate, looser policy for the author environment; the editor needs framing and inline
   execution that publish does not.
6. Only then enforce on publish, and keep the reporting endpoint.

Third-party scripts are the recurring cost: each vendor added to the allow-list can load anything it
likes from its own origin, so `script-src` becomes a list of parties you have transitively trusted.
Review it on the same cadence as dependency updates.
