Getting "Access to ... has been blocked by CORS policy" when a web font, JavaScript file, or fetch/XHR request loads from Amazon S3 or CloudFront? It happens because the browser enforces the Same-Origin Policy and your asset lives on a different origin (your CDN domain) than your page. The fix is to make the response carry the right CORS headers.
The quickest correct fix, in two parts:
- Configure CORS on the S3 bucket using the modern JSON format (the old
<CORSConfiguration>XML is legacy and no longer the console default). - Make CloudFront forward the
Originrequest header to S3 (via a Cache Policy / Origin Request Policy) and/or attach a CloudFront Response Headers Policy that adds the CORS headers at the edge — the modern recommended approach.
Below we cover the request flow, both layers of configuration with copy-paste config, and the troubleshooting that catches most teams (cached responses with the wrong headers).
What CORS is and why S3/CloudFront assets hit it
CORS (Cross-Origin Resource Sharing) is a browser security mechanism. Under the Same-Origin Policy, a page served from https://app.example.com can freely use resources from that same origin, but the browser restricts certain cross-origin requests unless the responding server explicitly opts in with Access-Control-Allow-Origin and related headers.
When you serve static assets — web fonts, bundled JS/CSS, images consumed via fetch(), or any XHR/fetch JSON — from an S3 bucket or a CloudFront distribution, those assets are on a different origin than your application. Common triggers:
- Web fonts referenced from CSS. Browsers always send a CORS request for fonts, so a missing
Access-Control-Allow-Originheader produces the classic font load failure and an empty/fallback typeface. <script crossorigin>,<link crossorigin>, or<img crossorigin>tags. Thecrossoriginattribute forces a CORS check; without proper headers you get a console error and broken Subresource Integrity (SRI) or WebGL textures.fetch()/XMLHttpRequestto a JSON file or API object stored in S3.
If the response lacks the matching CORS headers, the browser blocks JavaScript from reading it and logs "blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present". The S3 object is fine — it is the missing response headers that the browser objects to.
The request flow: simple vs preflight requests
Knowing which kind of request your browser is making tells you what to configure.
Simple requests (e.g. a GET/HEAD for a font or image, or a POST with a simple content type) go straight to the server. The browser sends an Origin header, and it expects the response to echo back an Access-Control-Allow-Origin that matches.
Preflighted requests are sent first as an OPTIONS request when the actual request is "non-simple" — for example a PUT/DELETE, a custom header like Authorization or x-amz-*, or a JSON content type. The browser asks "am I allowed to do this?" and the server must answer the OPTIONS with Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Max-Age before the real request is sent.
| Simple request | Preflighted request | |
|---|---|---|
| Triggered by | GET/HEAD/simple POST, no custom headers |
PUT/DELETE/PATCH, custom headers, JSON body |
| Extra round trip | None | An OPTIONS preflight first |
| S3 CORS must allow | the method + origin | the method, origin, and every requested header |
| Typical asset | fonts, images, CSS, JS | authenticated uploads, REST-style API objects |
This matters for S3: if a preflight OPTIONS is involved, your bucket CORS rule must list the relevant entries under AllowedMethods and AllowedHeaders, or the preflight fails before the real request is ever made.
Configure CORS on the S3 bucket (modern JSON format)
S3 CORS configuration is now expressed as JSON. The console's "Cross-origin resource sharing (CORS)" editor (Bucket → Permissions tab → Cross-origin resource sharing (CORS) → Edit) expects a JSON array of rules; the legacy <CORSConfiguration> XML format is deprecated for the console and only lingers in older tooling.
Each rule object supports these keys:
AllowedOrigins— origins permitted to read the response. Use exact origins likehttps://app.example.comin production;["*"]is convenient but allows any site to read your assets.AllowedMethods—GET,HEAD,PUT,POST,DELETEas needed.AllowedHeaders— request headers permitted on a preflight (e.g.Authorization,Content-Type,x-amz-*).["*"]allows all.ExposeHeaders— response headers JS is allowed to read (e.g.ETag,x-amz-request-id).MaxAgeSeconds— how long the browser may cache the preflight result. A larger value for rarely-changing static files cuts preflight chatter.
A solid starting point for serving fonts and static assets to a known origin:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": ["https://app.example.com"],
"ExposeHeaders": ["ETag", "Content-Length", "x-amz-request-id"],
"MaxAgeSeconds": 3600
}
]If you also accept browser-side uploads (presigned PUTs), add a second rule that allows the upload method and the headers your client sends:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": ["https://app.example.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 86400
},
{
"AllowedHeaders": ["Content-Type", "x-amz-acl", "x-amz-meta-*"],
"AllowedMethods": ["PUT", "POST"],
"AllowedOrigins": ["https://app.example.com"],
"ExposeHeaders": ["ETag", "x-amz-request-id"],
"MaxAgeSeconds": 86400
}
]You can apply the same JSON from the command line with the AWS CLI using aws s3api put-bucket-cors. Save the array above to cors.json and run:
# Apply a CORS configuration to a bucket
aws s3api put-bucket-cors \
--bucket my-assets-bucket \
--cors-configuration file://cors.json
# Read back the active CORS rules to confirm
aws s3api get-bucket-cors --bucket my-assets-bucketCloudFront: forward Origin and add a Response Headers Policy
CloudFront sits in front of S3 and caches responses. This is exactly where CORS quietly breaks: S3 only emits CORS headers when it actually sees an Origin request header, and by default CloudFront does not forward Origin to the origin. So CloudFront can cache a header-less response and serve it to every browser — even ones that send Origin.
There are two complementary fixes, and modern setups use both:
1. Forward the Origin header and vary the cache on it. Attach an Origin Request Policy (or a Cache Policy that includes Origin in the cache key) to the distribution behavior so CloudFront passes Origin through to S3 and stores a separate cached object per origin. AWS provides managed policies — Managed-CORS-S3Origin (origin request) and Managed-CORS-S3Origin / Managed-CachingOptimized combinations cover the common cases. This replaces the old "Whitelist Headers → Origin" toggle from the legacy behavior editor.
2. Attach a Response Headers Policy. This is the modern, recommended way to handle CORS at the CDN. A Response Headers Policy lets CloudFront add the CORS response headers itself (Access-Control-Allow-Origin, -Allow-Methods, -Allow-Headers, -Max-Age, etc.) without relying on S3 to produce them. AWS ships managed policies such as Managed-CORS-With-Preflight and Managed-SimpleCORS, or you can create a custom one. Attach it to the behavior in Distribution → Behaviors → Edit → Response headers policy.
| Approach | Where CORS headers come from | Best for | Watch out for |
|---|---|---|---|
| S3 bucket CORS (JSON) | S3 generates headers — but only if it sees Origin |
Direct-to-S3 access; the source of truth | Useless through CloudFront unless Origin is forwarded |
| CloudFront Origin Request / Cache Policy | Still S3, but CloudFront forwards Origin and varies cache |
Letting existing S3 CORS work through the CDN | Must include Origin in cache key or you cache the wrong response |
| CloudFront Response Headers Policy | CloudFront adds headers at the edge | Centralized control, multiple origins, overriding/adding headers | Can conflict with S3-emitted headers if both are set |
In practice: configure S3 CORS as the source of truth, attach the managed CORS origin-request/cache policy so Origin is forwarded and the cache is keyed correctly, and add a Response Headers Policy when you want the edge to guarantee the headers (or when the origin can't be relied on to send them).
You can create a CORS Response Headers Policy from the CLI. Define the config in a JSON file and pass it to create-response-headers-policy:
# cors-rhp.json
{
"Name": "assets-cors-with-preflight",
"Comment": "CORS headers for static assets served via CloudFront",
"CorsConfig": {
"AccessControlAllowOrigins": {
"Quantity": 1,
"Items": ["https://app.example.com"]
},
"AccessControlAllowHeaders": {
"Quantity": 1,
"Items": ["*"]
},
"AccessControlAllowMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
},
"AccessControlAllowCredentials": false,
"AccessControlExposeHeaders": {
"Quantity": 1,
"Items": ["ETag"]
},
"AccessControlMaxAgeSec": 3600,
"OriginOverride": true
}
}# Create the Response Headers Policy, then attach its Id to the
# distribution behavior (Console: Behaviors > Edit > Response headers policy,
# or via update-distribution).
aws cloudfront create-response-headers-policy \
--response-headers-policy-config file://cors-rhp.jsonHow S3 and CloudFront interact for CORS
The single most important rule: S3 returns CORS headers only when it sees a matching Origin header on the request. That has direct consequences through CloudFront:
- If CloudFront does not forward
Origin, S3 receives noOrigin, so it returns the object with no CORS headers — and CloudFront happily caches that header-less copy. - Once a header-less response is cached, every viewer gets it until the cache expires or you invalidate, regardless of what each viewer sends.
- When you forward
Originand include it in the cache key, CloudFront stores a separate cached variant per origin and emits aVary: Originresponse, so a browser from an allowed origin gets the rightAccess-Control-Allow-Origin.
If you'd rather not depend on the origin at all, a Response Headers Policy makes CloudFront the authority for CORS headers at the edge, which sidesteps the "did S3 see Origin?" problem entirely. This is the cleaner pattern when one distribution fronts several origins or when you serve assets to multiple known front-end domains. We cover the broader S3-plus-CloudFront delivery setup in our guide on efficient S3 and CloudFront CDN for faster loading, and the custom domain setup for CloudFront.
Troubleshooting CORS on S3 and CloudFront
- CORS works hitting S3 directly but not through CloudFront. Almost always the
Originheader isn't being forwarded, so a header-less response got cached. Attach the managed CORS origin-request/cache policy (or a Response Headers Policy) and invalidate the cached paths. - Cached wrong or missing CORS headers. Changes to S3 CORS or CloudFront policies don't retroactively fix already-cached objects. Run an invalidation:
aws cloudfront create-invalidation --distribution-id E123ABC --paths "/*". Use targeted paths in production to avoid invalidating everything. Vary: Originmatters. If you serve multiple allowed origins, make sureOriginis part of the cache key so CloudFront doesn't serve one origin'sAccess-Control-Allow-Originto another. A managed CORS policy handles this for you.- Preflight (
OPTIONS) failing. Confirm the S3 CORS rule lists the method underAllowedMethodsand every requested header underAllowedHeaders. If you use a Response Headers Policy with preflight, the managedManaged-CORS-With-Preflightpolicy responds toOPTIONScorrectly. - SigV4 / signed requests. If objects are private and accessed with SigV4 signed requests or signed CloudFront URLs/cookies, the
Authorizationandx-amz-*headers must be allowed inAllowedHeaders, and you'll typically needAccessControlAllowCredentialshandling — note that you cannot combineAccess-Control-Allow-Origin: *with credentials. - Headers duplicated or conflicting. If both S3 and a Response Headers Policy emit CORS headers, you can end up with duplicates. Pick one authority per header, or set
OriginOverridedeliberately so the policy wins.
These edge cases are where most teams lose an afternoon. At MicroPyramid, across 12+ years and 50+ delivered projects we've configured S3-plus-CloudFront asset delivery for sites of every size, so the moving parts — cache keys, origin forwarding, and Response Headers Policies — are familiar territory. If you'd like a hand hardening your AWS setup, our AWS consulting services and cloud migration services teams do exactly this.
Frequently Asked Questions
Why am I getting a CORS error from S3 or CloudFront?
Because the response your browser received is missing the Access-Control-Allow-Origin header (or it doesn't match your page's origin). The browser blocks JavaScript from reading the cross-origin resource. The object itself is fine — you need to configure CORS on the S3 bucket and ensure CloudFront forwards Origin or adds the headers via a Response Headers Policy.
How do I set CORS on an S3 bucket?
In the S3 console, open the bucket, go to the Permissions tab, find Cross-origin resource sharing (CORS), click Edit, and paste a JSON array of rules (each with AllowedOrigins, AllowedMethods, AllowedHeaders, ExposeHeaders, MaxAgeSeconds). The old XML <CORSConfiguration> format is legacy. From the CLI, use aws s3api put-bucket-cors --bucket NAME --cors-configuration file://cors.json.
Why does CORS work on S3 directly but not through CloudFront?
S3 only emits CORS headers when it sees an Origin request header. By default CloudFront does not forward Origin, so S3 returns a response with no CORS headers, and CloudFront caches that header-less copy for everyone. Fix it by attaching a managed CORS origin-request/cache policy (so Origin is forwarded and the cache is keyed on it) and invalidating the cached paths.
What is a CloudFront Response Headers Policy?
It's a CloudFront feature that lets the distribution add response headers — including CORS headers like Access-Control-Allow-Origin, -Allow-Methods, and -Max-Age — at the edge, without relying on the origin to produce them. AWS provides managed policies (Managed-SimpleCORS, Managed-CORS-With-Preflight), or you can create a custom one and attach it to a behavior. It's the modern, recommended way to manage CORS on a CDN.
How do I fix cached CORS headers?
Configuration changes don't rewrite already-cached objects. After updating S3 CORS or CloudFront policies, run an invalidation: aws cloudfront create-invalidation --distribution-id E123ABC --paths "/*" (or scope the paths). New requests then fetch fresh responses with the correct headers.
Do I need to configure CORS in both S3 and CloudFront?
Often, yes. Set CORS on S3 as the source of truth, then make CloudFront forward the Origin header so S3's headers survive caching. Alternatively, or in addition, attach a CloudFront Response Headers Policy to add the CORS headers at the edge. Using a Response Headers Policy removes the dependency on S3 seeing Origin, which is helpful when one distribution fronts multiple origins.