I needed one S3 bucket to serve assets to multiple domains. The request worked for one origin and then failed for another because the cached response carried the first site’s CORS header. The missing piece was Vary: Origin.
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>
This is the XML configuration from my 2016 setup. The current S3 console requires JSON; do not paste this XML into it. The first rule matches http:// and https:// origins and returns the specific origin in Access-Control-Allow-Origin. The second handles simple GET and HEAD requests with * as a fallback. Together, the rules make S3 vary the response by origin.
Testing it
The commands and responses below use substituted origins and bucket names. Supply a bucket you control; these are examples, not a live public test endpoint.
Send a HEAD request with both headers to inspect the response. This is not a browser preflight, which uses OPTIONS:
curl -sI \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: GET" \
https://example-bucket.s3.amazonaws.com/sample.xml
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
Notice Access-Control-Allow-Origin: https://example.com (the specific origin) and Vary: Origin. Now send a simple request with just the Origin header:
curl -sI \
-H "Origin: https://example.com" \
https://example-bucket.s3.amazonaws.com/sample.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.
Check the response headers These two rules produced
Vary: Originin my 2016 test. That observation does not establish that two rules are required in every configuration. A public, uncredentialed response withAccess-Control-Allow-Origin: *can be shared across origins without varying that header.
The separate http* and * rules are what triggered this behavior in my setup.
You need this when multiple sites load assets from the same S3 bucket, you’re using a CDN in front of S3, or the allowed-origin header changes with the request. Credentialed requests need an explicit allowed origin; the wildcard fallback shown here is not a credentialed-response policy. 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.
Was this useful?
Thanks for the feedback.