CORS on Nginx

⚠️ Security Warning: Using Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.
About the Vary Header: The 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.

Method 1: Simple Single Origin (Recommended)

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;
    }
}
    

Method 2: Multiple Origins with Simple Validation

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;
    }
}
    

Method 3: With Credentials (Cookies/Authentication)

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;
    }
}
    

Method 4: Advanced - Using Map Directive

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.

Method 5: Public API (Wildcard - Use with Caution)

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.

Configuration and Testing

Test your Nginx configuration syntax before reloading:

# Test configuration
nginx -t

# Reload Nginx
nginx -s reload
# or
sudo systemctl reload nginx
    

Testing Your CORS Configuration

For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.

Who’s behind this

Monsur Hossain and Michael Hausenblas

Contribute

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.

Buy the book

Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain