How to Implement Wildcard CORS for Multiple Subdomains in Nginx Configuration


2 views

When implementing CORS for multiple subdomains in Nginx, many developers encounter issues with regex pattern matching for the $http_origin variable. The common approach of using wildcard domains often fails due to subtle configuration nuances.

The core issue typically stems from three potential problems:

  1. The regex pattern might not account for both HTTP and HTTPS protocols
  2. The domain matching might be too strict about subdomain nesting
  3. The variable assignment might occur in the wrong context

Here's a tested solution that handles multiple subdomains:

server {
    listen 443;
    server_name api.example.com;

    # CORS handling
    set $cors "";
    if ($http_origin ~* ^https?://(.*\.)?example\.com(:[0-9]+)?$) {
        set $cors "true";
    }

    location / {
        if ($cors = "true") {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        }

        # Handle OPTIONS requests
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        proxy_pass http://backend;
    }
}

For more complex domain structures, consider these variations:

# Match specific subdomains
if ($http_origin ~* ^https?://(app|api|cdn)\.example\.com$) {
    set $cors "true";
}

# Match multiple domain levels
if ($http_origin ~* ^https?://([a-z0-9-]+\.)?example\.com(:[0-9]+)?$) {
    set $cors "true";
}

# Strict protocol matching
if ($http_origin ~* ^https://([a-z0-9-]+\.)*example\.com$) {
    set $cors "true";
}

CORS headers not appearing: Ensure the add_header directives are placed in the correct context. In Nginx, headers set in one block don't inherit to nested blocks.

Regex not matching: Test your pattern separately using online regex testers. Remember Nginx uses PCRE regex syntax.

SSL complications: When working with HTTPS, ensure your certificate covers all subdomains (wildcard cert or SAN certificates).

For high-traffic sites, avoid complex regex matching in the main context. Instead, consider:

map $http_origin $cors_origin {
    default "";
    "~^https?://([a-z0-9-]+\.)?example\.com(:[0-9]+)?$" "$http_origin";
}

server {
    # ...
    add_header 'Access-Control-Allow-Origin' $cors_origin;
}

Use curl to verify your CORS setup:

curl -H "Origin: http://sub.example.com" \
-I https://api.example.com/resource

Look for the Access-Control-Allow-Origin header in the response.




The core challenge occurs when trying to match dynamic subdomains in the $http_origin variable. Many developers struggle when their regex patterns fail to capture variations like:

https://api.example.com
http://static.example.com
https://dev.sub.example.com



The initial pattern if ($http_origin ~* (.*\.mydomain.com)) has several potential issues:
  1. It misses the protocol (http/https) portion
  2. Doesn't properly escape the dot character
  3. May conflict with Ansible variable substitution
Here's the corrected configuration that handles both HTTP and HTTPS subdomains: # CORS configuration for wildcard subdomains set $cors ""; if ($http_origin ~* ^https?://(?:[a-z0-9-]+\.)*example\.com(?:[:0-9]*)?$) { set $cors "true"; } location / { # Existing proxy configuration... if ($cors = "true") { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow_Credentials' 'true'; add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,X-CSRF-Token'; add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH'; } } For more complex domain structures: # Match multiple domain patterns if ($http_origin ~* ^https?://(?:[a-z0-9-]+\.)*(?:example\.com|api\.example\.net|dev\.example\.org)(?:[:0-9]*)?$) { set $cors "true"; } When using Ansible templates, ensure proper escaping: if ($http_origin ~* ^https?://(?:[a-z0-9-]+\.)*{{ domain | regex_escape }}(?:[:0-9]*)?$) { set $cors "true"; } Use curl to verify CORS headers: curl -I -H "Origin: https://test.example.com" https://your-server.com
  • Missing protocol in regex pattern
  • Improper escaping of dots in domain names
  • Case sensitivity issues (use ~* for case-insensitive matching)
  • Port numbers in origins (handled by the (?:[:0-9]*)? portion)