Setting Up CORS Vary-Origin Headers in AWS S3
When serving files from S3 to multiple domains, you need the Vary: Origin header. Without it, browsers and CDNs cache the first CORS response and serve it to all origins—breaking cross-origin requests for other sites.
Say you have assets in S3 that example.com and another.com both use. S3 returns Access-Control-Allow-Origin: example.com to the first request. A CDN caches that response. When another.com requests the same asset, it gets the cached response with the wrong origin, and the browser blocks it. Vary: Origin tells caches: "This response changes based on the Origin header. Don't reuse it for different origins."
The configuration
S3 generates Vary: Origin automatically when you configure multiple CORS rules:
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>http*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
</CORSRule>
</CORSConfiguration>
Go to your S3 bucket -> Permissions -> CORS configuration and paste this XML. The first rule handles preflight requests. It matches http* origins—meaning http:// or https:// URLs—and returns the specific origin in Access-Control-Allow-Origin. The second rule handles simple requests (GET/HEAD without preflight) using * as a fallback. Both rules together tell S3 to vary responses by origin.
Testing it
Send a preflight-style request with both headers:
curl -sI \
-H "Origin: https://kw.sg" \
-H "Access-Control-Request-Method: GET" \
https://s3.amazonaws.com/animate-vpaid-bridge/sample-1.xml
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://kw.sg
Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
Notice Access-Control-Allow-Origin: https://kw.sg (the specific origin) and Vary: Origin. Now send a simple request with just the Origin header:
curl -sI \
-H "Origin: https://kw.sg" \
https://s3.amazonaws.com/animate-vpaid-bridge/sample-1.xml
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
Now it returns Access-Control-Allow-Origin: * (wildcard), but Vary: Origin is still present. Caches will still differentiate responses by origin.
Tip
S3 only emits Vary: Origin when your CORS configuration has multiple rules. A single AllowedOrigin: * rule won't produce it, breaking CDN caching for multi-origin setups.
The http* and * combination in separate rules triggers this behavior.
You need this when multiple sites load assets from the same S3 bucket, you're using a CDN in front of S3, or you support credentials in cross-origin requests. If your assets are public and only used by one site, a simpler AllowedOrigin: * config works fine.
For a deeper dive into how CORS works and how to configure it in Apache and nginx, see my post on understanding CORS basics.
In 2025, I moved away from S3 to Cloudflare Workers for static hosting, which handles CORS configuration more simply and deploys to the edge globally.