Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.
Vary: Origin header is critical when your CORS headers change based on the origin. It prevents cache poisoning where one origin receives another origin's cached response. Always include it when using origin validation.
For most use cases, specify a single trusted origin:
location /api {
# Single trusted origin
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Vary' 'Origin' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
To allow multiple specific origins, use a simple regex check (issue #146):
location /api {
set $cors_origin "";
# Validate against allowed origins
if ($http_origin ~* (https?://(www\.)?example\.com|https?://app\.example\.com)) {
set $cors_origin $http_origin;
}
# Only set CORS headers if origin is allowed
add_header 'Vary' 'Origin' always;
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
When using credentials, you must specify exact origins (wildcards are not allowed):
location /api {
# Must use specific origin, not wildcard
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Vary' 'Origin' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
}
For complex origin validation rules, use the map directive. The following directive must be placed under http {} level (typically in nginx.conf):
http {
# Map for CORS origin validation
map '$request_method $http_origin' $allow_cors {
'~^OPTIONS https://(example\.com|app\.example\.com|api\.example\.com)$' OPTIONS;
'~^(GET|POST|PUT|DELETE) https://(example\.com|app\.example\.com|api\.example\.com)$' GET|POST|PUT|DELETE;
default 0;
}
# ... rest of http block
}
In your server/location block:
location /api {
if ($allow_cors = OPTIONS) {
# https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#cors_and_caching
add_header 'Vary' 'Origin' always;
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($allow_cors = GET|POST|PUT|DELETE) {
# https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#cors_and_caching
add_header 'Vary' 'Origin' always;
add_header 'Access-Control-Allow-Origin' $http_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Range' always;
}
}
Note: Replace all $http_origin with $scheme://$host if you need compatibility with Edge Legacy or Firefox <69.
Only use for completely public resources. This allows any website to access your API:
location /public-api {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
Source: Based on Michiel Kalkman's configuration, with security improvements.
Test your Nginx configuration syntax before reloading:
# Test configuration
nginx -t
# Reload Nginx
nginx -s reload
# or
sudo systemctl reload nginx
For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.
The content on this site stays fresh thanks to help from users like you! If you have suggestions or would like to contribute, fork us on GitHub.
Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain