Skip to content

Instantly share code, notes, and snippets.

@ryanburnette
Last active February 5, 2026 17:41
Show Gist options
  • Select an option

  • Save ryanburnette/d13575c9ced201e73f8169d3a793c1a3 to your computer and use it in GitHub Desktop.

Select an option

Save ryanburnette/d13575c9ced201e73f8169d3a793c1a3 to your computer and use it in GitHub Desktop.
Caddy v2.1+ CORS whitelist
(cors) {
@cors_preflight{args.0} method OPTIONS
@cors{args.0} header Origin {args.0}
handle @cors_preflight{args.0} {
header {
Access-Control-Allow-Origin "{args.0}"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers *
Access-Control-Max-Age "3600"
defer
}
respond "" 204
}
handle @cors{args.0} {
header {
Access-Control-Allow-Origin "{args.0}"
Access-Control-Expose-Headers *
defer
}
}
}
myawesomewebsite.com {
root * /srv/public/
file_server
import cors https://member.myawesomewebsite.com
import cors https://customer.myawesomewebsite.com
}
@mmm8955405
Copy link

(cors) {
        @cors_preflight{args.0} method OPTIONS
        @cors{args.0} header Origin {args.0}

        handle @cors_preflight{args.0} {
                header {
                        Access-Control-Allow-Origin "{args.0}"
                        Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
                        Access-Control-Allow-Headers *
                        Access-Control-Max-Age "3600"
                        defer   #turn on defer on your header directive to make sure the new header values are set after proxying
                }
                respond "" 204
        }

        handle @cors{args.0} {
                header {
                        Access-Control-Allow-Origin "{args.0}"
                        Access-Control-Expose-Headers *
                        defer
                }
        }
}
myawesomewebsite.com {
	root * /srv/public/
	file_server

	import cors https://member.myawesomewebsite.com
	import cors https://customer.myawesomewebsite.com
}

import cors https://member.myawesomewebsite.com
import cors https://customer.myawesomewebsite.com

Two errors reported

@ryanburnette
Copy link
Author

Thank you @C8opmBM and @mmm8955405. Gist update to reflect your suggestions.

@coolaj86
Copy link

coolaj86 commented Oct 19, 2023

@ryanburnette This is finally making it onto the Webi cheatsheet: https://webinstall.dev/caddy

(though right now it's just in preview at https://next.webinstall.dev/caddy)

@vanodevium
Copy link

When you want to enable CORS for ANY domain, you have to use next configuration:

This is really a very rare case, but in my practice I often configure the caddy in such a way that it stands behind the traefik and is responsible for different domains.

(cors) {
	@cors_preflight method OPTIONS

	header {
		Access-Control-Allow-Origin "{header.origin}"
		Vary Origin
		Access-Control-Expose-Headers "Authorization"
		Access-Control-Allow-Credentials "true"
	}

	handle @cors_preflight {
		header {
			Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE"
			Access-Control-Max-Age "3600"
		}
		respond "" 204
	}
}

http:// {
	root * /srv/public/
	file_server

	import cors {header.origin}
}

Feel free to change exposed headers, methods etc :)

@DurandA
Copy link

DurandA commented Feb 5, 2026

With the following configuration, only the first domain is matched with the following configuration:

api.example.com {
    reverse_proxy localhost:8090
    import cors https://example.com
    import cors http://localhost:3000
}

E.g. the server responded with:

HTTP/2 204 
access-control-allow-headers: Authorization, Content-Type
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
access-control-allow-origin: https://example.com
access-control-max-age: 3600
alt-svc: h3=":443"; ma=2592000
server: Caddy
date: Thu, 05 Feb 2026 17:30:29 GMT
X-Firefox-Spdy: h2

with the following request:

OPTIONS /token HTTP/2
Host: example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0
Accept: */*
Accept-Language: en,en-US;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization
Referer: http://localhost:3000/
Origin: http://localhost:3000
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
Priority: u=4
Pragma: no-cache
Cache-Control: no-cache
TE: trailers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment