
CloudFront Response Headers Policies Explained: Managed Policies, CORS, and the Override Flag

A response headers policy lets CloudFront add, remove, or override HTTP headers on the way out to the viewer — without touching your origin and without writing a CloudFront Function. It runs after the cache, so the headers you configure here don't affect cache keys and don't get stored in CloudFront's cache. That makes it the right tool for security headers, CORS, and one-off vendor headers, but the wrong tool for anything that should vary per object.
AWS ships five managed response headers policies. Two are CORS-only, one is security-only, and the remaining two are convenience bundles that combine CORS with the security set. If you're building a public website that needs HSTS and clickjacking protection, attach SecurityHeadersPolicy. If you're hosting fonts or assets cross-origin, SimpleCORS is usually enough. Anything more nuanced — a real Content-Security-Policy, custom vendor headers, browser requests that send JSON bodies or custom request headers, or Set-Cookie stripping — needs a custom policy.
| Managed policy | What it adds | Use when |
|---|---|---|
SimpleCORS | Access-Control-Allow-Origin: * | You serve assets that any origin can fetch with simple CORS requests |
CORS-With-Preflight | Access-Control-Allow-Methods, -Allow-Origin, -Expose-Headers (no Allow-Headers) | You want to use CORS with preflight by allowing necessary methods, no control over headers. |
SecurityHeadersPolicy | HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, X-XSS-Protection | You want a baseline of browser security headers without writing your own |
CORS-and-SecurityHeadersPolicy | SimpleCORS + SecurityHeadersPolicy | Public assets plus security baseline in one policy |
CORS-with-preflight-and-SecurityHeadersPolicy | CORS-With-Preflight + SecurityHeadersPolicy | Same caveat as CORS-With-Preflight: doesn't cover requests with custom headers or JSON bodies |
The rest of this article covers what each managed policy actually contains (down to header values), the four configuration sections of a custom policy, the override flag — which is the single biggest source of confusion — and the pitfalls that show up in real distributions.
This is the third part of a series that started with an overview of cache, origin request, and response policies, continued with a deep dive on cache policies, and a deep dive on origin request policies. Read those first if you want context on how the three policy types fit together.
What a response headers policy does — and what it doesn't
CloudFront applies a response headers policy before sending the response to the viewer, after the response has either come back from the origin or been served from the cache. The headers added or removed by the policy don't get stored alongside the cached object, so once a policy change has propagated through the distribution, viewers see the new headers without you needing to invalidate cached objects. That's a real advantage over baking headers into the origin or stuffing them into a Lambda@Edge function.
A few things a response headers policy explicitly does not do. It can't read or modify the response body. It can't set headers conditionally based on URL patterns within a single cache behavior — for that, use multiple cache behaviors with different policies attached, or fall back to a CloudFront Function. AWS documentation states that CloudFront modifies headers in responses served from the cache and in responses forwarded from the origin; what's worth verifying in your own distribution is whether your custom error responses (and any CloudFront-generated error pages) pick up the policy as expected, since edge cases there are not exhaustively documented.
The policy attaches to a cache behavior, not to a distribution as a whole. A distribution with five cache behaviors can use five different response headers policies, which is how you separate the headers needed for /api/* from those needed for /static/*.
The five managed response headers policies
SimpleCORS
Policy ID: 60669652-455b-4ae9-85a4-c4c02393f86c
SimpleCORS adds a single header: Access-Control-Allow-Origin: *. That's it. It covers what the CORS spec calls "simple requests" — GET, HEAD, or POST using only CORS-safelisted request headers and a Content-Type of application/x-www-form-urlencoded, multipart/form-data, or text/plain. A simple cross-origin GET that happens to return JSON in the response body is fine; what makes a request non-simple is request-side characteristics. So application/json as a request body triggers a preflight (browsers send Access-Control-Request-Headers: content-type), as do custom headers like Authorization. Browser requests with either are out of scope for SimpleCORS and also for the managed CORS-With-Preflight, as the next section explains.
The override flag on this policy is set to false. If your origin already sets Access-Control-Allow-Origin, CloudFront uses the origin's value instead of replacing it with *. That's usually the right default: an origin that already does CORS knows what it wants to allow, and overwriting it with * could break credentialed requests that the origin had configured correctly.
Use this policy for fonts, images, and static assets where you want any browser to fetch the file across origins. Don't use it for API endpoints — preflight requests will fail because there's no Access-Control-Allow-Methods header in the response.
CORS-With-Preflight
Policy ID: 5cc3b908-e619-4b99-88e5-2cf7f45965bd
CORS-With-Preflight adds three CORS response headers, with these exact values per AWS:
| Header | Value |
|---|---|
Access-Control-Allow-Methods | DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT |
Access-Control-Allow-Origin | * |
Access-Control-Expose-Headers | * |
For preflight requests (using the HTTP OPTIONS method), all three headers are added. For simple CORS requests, only Access-Control-Allow-Origin is added.
The notable absence is Access-Control-Allow-Headers. Per the CORS spec, when a preflight request includes Access-Control-Request-Headers, the preflight response is required to include Access-Control-Allow-Headers listing those headers (or * for non-credentialed requests). This managed policy does neither, so any preflight asking permission for a non-safelisted request header will be rejected by the browser.
In practice this rules out the most common API case: any request with Content-Type: application/json triggers a preflight with Access-Control-Request-Headers: content-type, which CORS-With-Preflight cannot satisfy. Same for any request that sends Authorization, X-Api-Key, or any other custom header. The policy is genuinely only useful when the preflight is triggered by the method alone (PUT, DELETE, etc.) and the request body uses one of the three CORS-safelisted content types — which is rare for real APIs.
Like SimpleCORS, this policy has origin override set to false. There's a subtlety here that bites people: when override is false and the origin sends any CORS header — even one not listed in the policy — CloudFront uses the origin's CORS headers and skips all the CORS headers from the policy. This is documented behavior, not a bug. Treat policy CORS and origin CORS as mutually exclusive unless you've tested the combined behavior.
For a browser request that sends a JSON body, Authorization, or any other non-safelisted header, write a custom policy with an explicit access_control_allow_headers list. The combined CORS-with-preflight-and-SecurityHeadersPolicy has the same limitation, since it inherits its CORS section from CORS-With-Preflight.
SecurityHeadersPolicy
Policy ID: 67f7725c-6f97-4210-82d7-5512b31e9d03
SecurityHeadersPolicy adds five security-related headers with sensible browser defaults. The exact values applied are:
| Header | Value |
|---|---|
Strict-Transport-Security | max-age=31536000 |
X-Content-Type-Options | nosniff |
X-Frame-Options | SAMEORIGIN |
Referrer-Policy | strict-origin-when-cross-origin |
X-XSS-Protection | 1; mode=block |
A few things to know about these defaults before you attach the policy and call it done.
Strict-Transport-Security is set to one year (max-age=31536000) but does not include includeSubDomains or preload. If you want either, you need a custom policy. Modern HSTS guidance generally recommends max-age=63072000 (two years) with includeSubDomains and preload for production, though the one-year default is safer if you're not sure every subdomain serves over HTTPS.
X-Frame-Options: SAMEORIGIN allows your own pages to embed each other in iframes but blocks third-party embedding. If you need stricter clickjacking protection, use a custom policy with frame-ancestors 'none' in your CSP and X-Frame-Options: DENY.
X-XSS-Protection: 1; mode=block is included for older browsers, but modern browsers ignore it entirely in favor of CSP. It doesn't hurt, but don't expect it to do much in 2026.
The policy does not include a Content-Security-Policy header. CSP is too application-specific for AWS to ship a default — you'll need a custom policy if you want one. This is the single biggest gap in SecurityHeadersPolicy and the most common reason teams end up writing their own.
For all five of these headers, override is false, except for X-Content-Type-Options: nosniff, which CloudFront adds whether or not the origin sends it. The other four are skipped if the origin already sets the header.
CORS-and-SecurityHeadersPolicy
Policy ID: e61eb60c-9c35-4d20-a928-2b84e02af89c
This is SimpleCORS plus SecurityHeadersPolicy rolled into one. Use it when you need both — typically for a public asset bucket fronted by CloudFront where you want browsers to be able to fetch fonts cross-origin and you want HSTS turned on.
There's no functional difference compared to attaching the two policies separately, because you can't anyway: a cache behavior accepts exactly one response headers policy. The combined version exists so you don't have to write a custom policy just to merge two managed ones.
CORS-with-preflight-and-SecurityHeadersPolicy
Policy ID: eaab4381-ed33-4a86-88ca-d9558dc6cd63
The same idea, but using AWS's managed preflight CORS policy instead of SimpleCORS. It inherits the CORS limitation from CORS-With-Preflight: no Access-Control-Allow-Headers, so it doesn't work for browser requests sending Authorization, Content-Type: application/json, or any other non-safelisted header. Use it for public endpoints where the preflight is triggered by method alone.
What's inside a custom response headers policy
A custom policy has four configurable sections plus a Server-Timing toggle. Each section is independent — you can use one, all, or none.
CORS config
The CORS section maps directly to the CORS response headers. You set the allowed origins, methods, headers, exposed headers, and max-age, plus whether credentials are allowed. The fields and the headers they produce are:
| Field | Header produced |
|---|---|
access_control_allow_origins | Access-Control-Allow-Origin |
access_control_allow_methods | Access-Control-Allow-Methods |
access_control_allow_headers | Access-Control-Allow-Headers |
access_control_expose_headers | Access-Control-Expose-Headers |
access_control_max_age_sec | Access-Control-Max-Age |
access_control_allow_credentials | Access-Control-Allow-Credentials |
A wildcard * is allowed for origins, methods, and headers, but the Authorization header is the exception: AWS does not let it match a wildcard in Access-Control-Allow-Headers. If your viewers send Authorization headers, list it explicitly. This is separate from the credentials/wildcard issue — even on non-credentialed requests, Authorization won't be covered by *.
For credentialed CORS — anything sending cookies or using credentials: 'include' in fetch — Access-Control-Allow-Origin: * is invalid. Browsers reject the response. Use explicit origins (you can list multiple) and set access_control_allow_credentials = true. None of the managed policies do this; it requires a custom policy.
The CORS section also has its own origin_override flag, which controls whether CloudFront overrides CORS headers received from the origin. The semantics are stricter than they might appear: when origin_override = false and the origin sends any CORS header — even one not in the policy — CloudFront passes through the origin's CORS headers and skips all CORS headers from the policy. There's no per-header merging. Set it to true if CloudFront is the source of truth for CORS, and false if your origin is doing CORS correctly already.
Security headers config
This section is a curated set of six well-known security headers. You can configure any subset of:
Content-Security-Policy— directives string, plusoverrideX-Content-Type-Options— boolean toggle (the value is alwaysnosniff), plusoverrideX-Frame-Options—DENYorSAMEORIGIN, plusoverrideReferrer-Policy— one of eight standard values, plusoverrideStrict-Transport-Security—max-ageseconds, plusincludeSubDomainsandpreloadbooleans, plusoverrideX-XSS-Protection— protection on/off, mode-block on/off, optional report URI, plusoverride
The reason this section exists separately from custom headers is input validation. CloudFront knows that X-Frame-Options is one of two values and Referrer-Policy is one of eight, and it validates accordingly. If you typo SAMEORIGIN as SAME_ORIGIN, the API call fails at deploy time instead of producing a header that browsers ignore at runtime.
This is the section to use for HSTS, clickjacking protection, and CSP. A custom Terraform example for a strict configuration:
resource "aws_cloudfront_response_headers_policy" "strict" { name = "strict-security-headers" security_headers_config { strict_transport_security { access_control_max_age_sec = 63072000 # 2 years include_subdomains = true preload = true override = true } content_security_policy { content_security_policy = "default-src 'self'; frame-ancestors 'none'; object-src 'none'" override = true } frame_options { frame_option = "DENY" override = true } content_type_options { override = true } referrer_policy { referrer_policy = "strict-origin-when-cross-origin" override = true } } }
A note on CSP: it's the one security header where the cost of being wrong is high — too strict and you break your own site, too loose and it provides no protection. Roll it out in Content-Security-Policy-Report-Only mode first (which CloudFront doesn't support natively as of writing — you'll need a custom header in the next section to do that) and watch the reports before switching to enforcement.
There's also a hard limit to know about: AWS caps the Content-Security-Policy header value at 1,783 characters. For complex CSPs with many script-src and style-src allowances, you can hit this. If you do, the practical workarounds are setting the CSP outside of the response headers policy (at the origin, or via a CloudFront Function or Lambda@Edge that has its own size limits to check) and tightening the policy itself with hashes or nonces rather than long allowlists.
Custom headers config
This section adds arbitrary key-value pairs to the response. Each entry has a header name, a value, and an override flag.
Use this for headers that don't fit the CORS or security set: Permissions-Policy, Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy, vendor-specific headers, build identifiers, anything else. There's no validation here beyond basic syntax — CloudFront takes whatever string you give it.
A typical addition for a modern hardened site:
custom_headers_config { items { header = "Permissions-Policy" value = "geolocation=(), microphone=(), camera=()" override = true } items { header = "Cross-Origin-Opener-Policy" value = "same-origin" override = true } items { header = "Cross-Origin-Embedder-Policy" value = "require-corp" override = true } }
Custom headers also sneak in Content-Security-Policy-Report-Only if you want to test a CSP before enforcing it — set the header name to Content-Security-Policy-Report-Only and put the directives in the value.
Remove headers config
This is a relatively recent addition. It strips named headers out of the response before CloudFront sends it to the viewer. Common targets are X-Powered-By, Vary, and origin-leaked debug headers like X-AspNet-Version. For an S3 origin, you might want to strip x-amz-server-side-encryption or other x-amz-* headers that leak implementation details.
remove_headers_config { items { header = "Server" } items { header = "X-Powered-By" } }
Server and Date deserve special mention. AWS does let you remove the origin-supplied versions of both — but CloudFront immediately adds its own. For Server, the value CloudFront adds is literally CloudFront. So removing Server doesn't truly hide that you're behind a CDN; it just changes which name leaks.
Removal happens before the policy's other headers are added, so you can both strip a header from the origin and add your own version with a different value through the custom-headers section. Several headers are protected and the API rejects attempts to remove them at deploy time. The full list per AWS docs: Connection, Content-Encoding, Content-Length, Expect, Host, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, Proxy-Connection, Trailer, Transfer-Encoding, Upgrade, Via, Warning, X-Accel-* (multiple variants), X-Amz-Cf-*, X-Amzn-* (multiple variants), X-Cache, X-Edge-*, X-Forwarded-Proto, and X-Real-Ip. Most of these are protocol-level or CloudFront-internal.
Server-Timing headers
The Server-Timing section is separate from the others and produces a single header: Server-Timing. When enabled, CloudFront emits diagnostic timing data — cache hit/miss status, time spent on various phases of the request — that you can read from the browser's Performance API. The configuration accepts a sampling rate from 0 to 100, expressed as a percentage with up to four decimal places.
There's a useful escape hatch for production debugging: even when sampling is set to 0, any request that includes Pragma: server-timing in the request headers gets a Server-Timing response header. That means you can leave the policy attached at 0% sampling and still pull timing data on demand from a browser devtools or a curl command without affecting other viewers.
This is mostly useful for debugging slow requests in production. Don't enable it at 100% sampling on a high-traffic distribution unless you want every viewer to see the timing data, which can be a minor information leak about your infrastructure. A few percent is usually enough to catch patterns; the Pragma header covers targeted debugging.
When override does what
The override flag is the single biggest source of confusion in response headers policies, and it's worth understanding precisely. For each header in a policy, override controls what happens when both the origin and the policy specify the same header.
override | Origin sends header | Origin doesn't send header |
|---|---|---|
true | Policy value wins | Policy value is added |
false | Origin value passes through | Policy value is added |
A common mistake: setting override = false on Strict-Transport-Security and assuming the policy's HSTS value will reach the viewer. If your origin sets Strict-Transport-Security: max-age=0 (which some misconfigured origins do), the origin's max-age=0 will pass through and disable HSTS in the browser. Set override = true for security headers unless you have a specific reason not to.
The SecurityHeadersPolicy managed policy uses override = false for everything except X-Content-Type-Options. That's a conservative default but it means the policy is only as good as your origin's behavior on those headers. If you want guarantees, write a custom policy and set override = true.
Common pitfalls
Headers don't appear on cached responses immediately after policy changes. They actually do — response headers policies don't affect the cache, so changes take effect on the next viewer request. If you're not seeing the change, your browser is caching the old response. Hard-refresh, or check with curl -I from a clean session.
Access-Control-Allow-Origin: * plus credentials silently fails. For cookie-based or otherwise credentialed CORS (using credentials: 'include' in fetch), browsers reject responses with Access-Control-Allow-Origin: *. Use explicit origins and set Access-Control-Allow-Credentials: true. None of the managed CORS policies do this; a custom policy is required.
Authorization header is silently rejected even with Allow-Headers: *. This is a separate issue from credentials. AWS does not let Authorization match the wildcard in Access-Control-Allow-Headers — it must be listed by name. If you're sending bearer tokens in Authorization, your custom policy needs Authorization explicitly in its allowed-headers list.
Origin's Access-Control-Allow-Origin overrides your policy when you didn't expect it. Check the override flag. The managed CORS policies all have origin override set to false. The behavior is stricter than people assume: if the origin sends any CORS header, even one you didn't list in the policy, CloudFront uses the origin's CORS headers and skips all of yours. To make CloudFront's CORS values authoritative, write a custom policy with origin_override = true.
SecurityHeadersPolicy is missing CSP. It's not a bug — CSP is too application-specific for a managed default. You'll need a custom policy with the content_security_policy block in the security headers config to get one. Watch the 1,783-character limit if your CSP is long.
HSTS is set but includeSubDomains is missing. The managed SecurityHeadersPolicy doesn't include either includeSubDomains or preload. If you want a serious HSTS deployment, write a custom policy.
Origin error responses don't carry the headers you expect. Worth verifying: how custom error responses and CloudFront's own generated errors (502s on origin failure, for example) interact with response headers policies isn't exhaustively documented. Test the failure paths in your distribution before assuming the headers are there.
Server header still leaks despite the remove-headers config. Removing Server doesn't actually remove the header — CloudFront strips the origin's value and substitutes its own value of CloudFront. Same applies to Date. If you want to hide the CDN entirely, response headers policies are the wrong tool.
Headers like X-Cache or Via won't strip. These are on the protected list. Other surprising entries on the non-removable list include Content-Length, Connection, Transfer-Encoding, X-Forwarded-Proto, and X-Real-Ip. The API rejects them at deploy time, not at runtime.
Server-Timing data is leaking to viewers in production. If you enabled it for debugging and forgot to turn it off or lower the sampling rate, every visitor is getting cache-status and infrastructure timing data. Set sampling to 0 and use the Pragma: server-timing request header for on-demand debugging instead.
What's next
The short version of this article: use SecurityHeadersPolicy if you want a baseline and don't care about CSP, use SimpleCORS for cross-origin static assets and simple GET reads, and write a custom policy for almost everything else — browser requests that send JSON bodies or custom headers, credentialed CORS, real CSP, or stripping leaky origin headers. The managed CORS-With-Preflight policy is narrower than its name suggests; treat it as a starting point that usually needs replacement. The combined CORS-plus-security policies exist for convenience and have no special powers beyond the sum of their parts.
This wraps up the policy series — cache, origin request, and now response. If you're building a new CloudFront distribution from scratch, the right mental model is: cache policy decides what makes a request unique and how long to keep it, origin request policy decides what reaches your backend, response headers policy decides what reaches the viewer. Keep those three concerns separate and most of the weird CloudFront bugs go away.
Frequently Asked Questions
What's the difference between a CloudFront response headers policy and a CloudFront Function?
A response headers policy is a declarative, no-code way to add, remove, or override HTTP response headers on a cache behavior. A CloudFront Function runs custom JavaScript and can do more (like conditional logic based on the request), but requires you to write and deploy code. Use a response headers policy first; reach for a Function only when the policy's four sections can't express what you need.
Which AWS managed CloudFront response headers policy should I use for CORS?
Use SimpleCORS for public static assets that only need simple cross-origin GET/HEAD/POST requests. Use CORS-With-Preflight only if your preflight is triggered by method alone (PUT, DELETE) with a CORS-safelisted content type — it doesn't include Access-Control-Allow-Headers, so it fails for JSON bodies or custom headers like Authorization. For anything more, write a custom policy.
Why doesn't AWS's SecurityHeadersPolicy include a Content-Security-Policy header?
CSP is too application-specific for AWS to ship a safe default — the right directives depend entirely on which scripts, styles, and origins your site actually uses. You need to add a Content-Security-Policy via a custom response headers policy's security_headers_config section.
Why is my Access-Control-Allow-Origin: * header being ignored by the browser when I send credentials?
Browsers reject Access-Control-Allow-Origin: * on any request that sends credentials (cookies, or fetch with credentials: 'include'). You need a custom CloudFront response headers policy with explicit allowed origins and access_control_allow_credentials set to true — none of the AWS managed CORS policies support credentialed requests.
Why does my CORS preflight fail even though Access-Control-Allow-Headers is set to *?
The Authorization header is the one exception AWS makes: it can't be matched by a wildcard in Access-Control-Allow-Headers. If your requests send an Authorization header, you must list it explicitly in your custom response headers policy's allowed-headers configuration.
Does a CloudFront response headers policy affect what gets cached?
No. Response headers policies apply after the cache lookup, so the headers they add or remove are never stored as part of the cached object. That also means a policy change takes effect on the very next viewer request without needing a cache invalidation.
What does the override flag do in a CloudFront response headers policy?
For each configured header, override controls what wins when the origin also sends that header: override = true means the policy's value always wins, override = false means the origin's value passes through untouched if present (and the policy's value is only added when the origin doesn't send it). Most AWS managed policies default override to false except for X-Content-Type-Options.
Why can't I remove the Server header from CloudFront responses?
You can strip the origin's Server header value using a remove_headers_config, but CloudFront immediately substitutes its own Server: CloudFront header afterward — so the header never actually disappears, it just stops leaking your origin's server software. The same applies to the Date header.
What's the maximum length of a Content-Security-Policy header in a CloudFront response headers policy?
AWS caps the Content-Security-Policy value at 1,783 characters. If your CSP needs more allowlisted sources than that, set it at the origin instead, or tighten the policy using hashes or nonces rather than long script-src/style-src allowlists.
Building or auditing AWS infrastructure?
u11d works with engineering teams on AWS architecture, from CDN and edge configuration to full infrastructure reviews. If CloudFront is just one piece of a bigger AWS setup you'd like reviewed, let's talk.






