How can I allow Multiple Domain Origins in CORS

1. The problem I’m having:

I want to configure Caddy in such a way that will allow me to activate CORS on multiple domains. I’ve been successful in adding the direct CORS config. And apply it to 1 domain/per line at a time. My problem is that sometimes I have a lot of servers that are trying to access my app on caddy, and many of them are with different domains. I find it really inefficient to add multiple lines every time I need to add a new domain. So I thought I’d ask my question here

2. Error messages and/or full log output:

There are no specific errors, but I'm looking for a convenient way to add multiple domains to apply CORS on

3. Caddy version:

I’m using: caddy:2.7.4-alpine

4. How I installed and ran Caddy:

a. System environment:

I am using Caddy Docker container with this version/tag: caddy:2.7.4-alpine

b. Command:

docker-compose up -d

c. Service/unit/compose file:

  caddy:
    image: ghcr.io/myrepo/my-caddy-build:latest
    container_name: caddy-reverse-proxy
    restart: unless-stopped
    networks:
      - giveth
    ports:
      - 80:80
      - 443:443
    env_file:
      - ../.env
    environment:
      MY_APP_URL: ${MY_APP_URL:-}
      RESTRICTED_PATHS: ${RESTRICTED_PATHS:-}
      IP_WHITELIST: ${IP_WHITELIST:-}
      WHITELIST_RATE_EVENTS: ${IG_WHITELIST_RATE_EVENTS:-}
      WHITELIST_RATE_INTERVAL: ${IG_WHITELIST_RATE_INTERVAL:-}
      PUBLIC_RATE_EVENTS: ${IG_PUBLIC_RATE_EVENTS:-}
      PUBLIC_RATE_INTERVAL: ${IG_PUBLIC_RATE_INTERVAL:-}
      DOMAIN_WHITELIST: ${DOMAIN_WHITELIST:-}
    volumes:
      - caddy_data:/data
      - caddy_config:/config
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ../logs/caddy:/usr/src/app/
    depends_on:
      - postgres

AND… I’m using a .env file to load ENV Variables into the configuration

#################### Public URLs configuration ####################
MY_APP_URL=https://myappdomain.com
IP_WHITELIST="IP1 IP2 IP3"
RESTRICTED_PATHS="/critical /restricted"
## Events(Number)/Interval(s,m,h,d) For Whitelisted IP Addresses Specified above
WHITELIST_RATE_EVENTS=1000000
WHITELIST_RATE_INTERVAL=1m
## Events(Number)/Interval(s,m,h,d) allowed for public Connections
PUBLIC_RATE_EVENTS=100
PUBLIC_RATE_INTERVAL=1m
####################################################################
DOMAIN_WHITELIST="https://domain1.com https://domain2.com" [NOT WORKING]

NOT that the last line of config is what is not working. I can’t specify multiple domains for CORS origin

d. My complete Caddy config:

# Global Options
{
	order rate_limit before basicauth
	log global {
		output file /usr/src/app/global.log
		format json
		level debug
	}
}

# CORS Config Block Directive
(cors) {
  @cors_preflight{args[0]} {
    method OPTIONS
    header Origin {args[0]}
  }
  @cors{args[0]} header Origin {args[0]}

  handle @cors_preflight{args[0]} {
    header {
      Access-Control-Allow-Origin "{args[0]}"
      Access-Control-Allow-Credentials true
      Access-Control-Allow-Headers "Authorization, Cache-Control, Content-Type"
      Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE"
      Access-Control-Max-Age "3600"
      Vary Origin
      defer
    }
    respond "" 204
  }

  handle @cors{args[0]} {
    header {
      Access-Control-Allow-Origin "{args[0]}"
      Access-Control-Allow-Credentials true
      Access-Control-Expose-Headers *
      Vary Origin
      defer
    }
  }
}

#--------------------------------------------------------------------------
# My App site block
#--------------------------------------------------------------------------
{$APP_URL} {
    # Call the cors for whitelisted domains
    import cors {$DOMAIN_WHITELIST}
    
    # Configure Logging
    log {
        output file /usr/src/app/access.log
        format json
    }
    
    # Identify Config Keys for accesses
    @privateIPAccess remote_ip {$IP_WHITELIST} # Whitelisted IP Addresses
    @publicIPAccess not remote_ip {$IP_WHITELIST} # Unwhitelisted IP Addresses
	@restrictedPaths path {$RESTRICTED_PATHS} # Restricted Paths
	@unRestrictedPaths not path {$RESTRICTED_PATHS} # Unrestricted Paths

    # Handling Restricted Paths Routes
	route @restrictedPaths {
	  respond @publicIPAccess 403
	  reverse_proxy my-app:3000 {
        transport http {
            response_header_timeout 300s
            dial_timeout 300s
        }
      }
	}

    # Handling Unrestricted Paths Route
	route @unRestrictedPaths {
        reverse_proxy my-app:3000 {
            transport http {
                response_header_timeout 300s
                dial_timeout 300s
            }
        }
        rate_limit @privateIPAccess {
            zone myzone {
                key    {remote_host}
                events {$WHITELIST_RATE_EVENTS}
                window {$WHITELIST_RATE_INTERVAL}
            }
            sweep_interval 1m
        }
	}

	# Apply Global rate limiting to all public Requests
	rate_limit @publicIPAccess {
        zone myzone {
            key    {remote_host}
            events {$PUBLIC_RATE_EVENTS}
            window {$PUBLIC_RATE_INTERVAL}
        }
        sweep_interval 1m
    }

    ## Extra Header Configs
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        X-Frame-Options "DENY"
    }

    ## Request Body Size
    request_body {
    max_size 30MB
    }
}

5. Links to relevant resources:

You’ll need to do this instead:

import cors {$DOMAIN_WHITELIST_1}
import cors {$DOMAIN_WHITELIST_2}

I don’t see how it could work from a single env var.

1 Like

Thanks @francislavoie for this. I was wondering if a wildcard subdomain can work with this kind of configuration. Something like this:

import cors https://*.mydomaon.io

And let this apply to all subdomains.

If it is not possible, I am wondering if this can be added as a feature request

No because the header matcher doesn’t support * infix matching. See Request matchers (Caddyfile) — Caddy Documentation and setting the header values to https://*.mydomaon.io is not valid either.

Read what your snippet is doing, it’s not possible for the argument to be a wildcard, it needs to be a real actual value.

Thanks for the insight @francislavoie.

Knowing that this approach was not possible. I think I have found a workaround that works pretty well, that involves regex expressions.

I’ll share it below for the community so that whoever is dealing with the same situation, can benefit

NOTE: An example for this case, is when you have a backend and you want a frontend to access it that is hosted on a PAAS Hosting Services that gives you a random DOMAIN for your frontend (Like VERCEL)

The below snippet works with Vercel + Localhost + YOUR_OWN_WHITELISTED_HOSTNAMES

# Global Options
{
	order rate_limit before basicauth
	log global {
		output file /usr/src/app/global.log
		format json
		level debug
	}
}

# CORS Config Block Directive
(cors) {
    @cors_preflight {
        method OPTIONS
    }
    @corsOrigin {
        header_regexp Origin ^https?://([a-zA-Z0-9-]+\.)*vercel\.app$|^https?://localhost(:[0-9]+)?$|^https?://({$DOMAIN_WHITELIST})$
    }

    handle @cors_preflight {
        header {
            Access-Control-Allow-Origin "{http.request.header.Origin}"
            Access-Control-Allow-Credentials true
            Access-Control-Allow-Headers "*"
            Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE"
            Access-Control-Max-Age "3600"
            Vary Origin
            defer
        }
        respond "" 204
    }

    handle @corsOrigin {
        header {
            Access-Control-Allow-Origin "{http.request.header.Origin}"
            Access-Control-Allow-Credentials true
            Access-Control-Expose-Headers "*"
            Vary Origin
            defer
        }
    }
}

#--------------------------------------------------------------------------
# My App site block
#--------------------------------------------------------------------------
{$APP_URL} {
    # Call the cors for whitelisted domains
    import cors
    
    # Configure Logging
    log {
        output file /usr/src/app/access.log
        format json
    }
    
    # Identify Config Keys for accesses
    @privateIPAccess remote_ip {$IP_WHITELIST} # Whitelisted IP Addresses
    @publicIPAccess not remote_ip {$IP_WHITELIST} # Unwhitelisted IP Addresses
	@restrictedPaths path {$RESTRICTED_PATHS} # Restricted Paths
	@unRestrictedPaths not path {$RESTRICTED_PATHS} # Unrestricted Paths

    # Handling Restricted Paths Routes
	route @restrictedPaths {
	  respond @publicIPAccess 403
	  reverse_proxy my-app:3000 {
        transport http {
            response_header_timeout 300s
            dial_timeout 300s
        }
      }
	}

    # Handling Unrestricted Paths Route
	route @unRestrictedPaths {
        reverse_proxy my-app:3000 {
            transport http {
                response_header_timeout 300s
                dial_timeout 300s
            }
        }
        rate_limit @privateIPAccess {
            zone myzone {
                key    {remote_host}
                events {$WHITELIST_RATE_EVENTS}
                window {$WHITELIST_RATE_INTERVAL}
            }
            sweep_interval 1m
        }
	}

	# Apply Global rate limiting to all public Requests
	rate_limit @publicIPAccess {
        zone myzone {
            key    {remote_host}
            events {$PUBLIC_RATE_EVENTS}
            window {$PUBLIC_RATE_INTERVAL}
        }
        sweep_interval 1m
    }

    ## Extra Header Configs
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        X-Frame-Options "DENY"
    }

    ## Request Body Size
    request_body {
    max_size 30MB
    }
}

And then in the ENV file of you docker-compose, you can specify the whitelisted domains, that CORS will be applied to separated by “|” as seen below:

####################################################################
DOMAIN_WHITELIST="domain1.com|domain2.com|sub.domain3.com"

Hope this helps anyone who is facing the same challenge

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.