A Traefik middleware plugin that dynamically sets CORS headers to support wildcard subdomain matching.
Traefik's built-in CORS middleware doesn't support dynamic Access-Control-Allow-Origin headers. When you configure Traefik's built-in CORS:
# β Traefik's built-in CORS (doesn't work for wildcard subdomains)
http:
middlewares:
cors:
headers:
accessControlAllowOriginList:
- "https://*.fndo.me"Traefik returns the literal string https://*.fndo.me in the response header, which browsers reject because CORS spec requires either:
- An exact origin:
https://bigrob.fndo.meβ - A wildcard:
*β - NOT a pattern:
https://*.fndo.meβ
This plugin solves this by:
- Reading the
Originheader from the request - Matching it against your wildcard patterns
- Returning the exact origin in
Access-Control-Allow-Origin
Perfect for SaaS platforms where each tenant has their own subdomain:
https://shop1.example.comhttps://shop2.example.comhttps://*.example.com(unlimited shops)
All subdomains can call your API at https://api.example.com with proper CORS.
- β
Wildcard subdomain matching (
https://*.example.com) - β Multiple pattern support
- β
Exact domain matching (
https://example.com) - β
Port-aware matching (
https://*.example.com:8080) - β Credentials support (optional)
- β Preflight request handling
- β Configurable methods, headers, max-age
Add the plugin to your Traefik static configuration:
traefik.yml
experimental:
plugins:
cors-dynamic-subdomain:
moduleName: "github.com/x-ream/cors-dynamic-subdomain"
version: "v0.1.0"Or via command line:
--experimental.plugins.cors-dynamic-subdomain.modulename=github.com/x-ream/cors-dynamic-subdomain
--experimental.plugins.cors-dynamic-subdomain.version=v0.1.0Configure the middleware in your dynamic configuration:
Kubernetes CRD:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: cors-dynamic
spec:
plugin:
cors-dynamic-subdomain:
# β
This plugin uses 'allowedOriginPatterns' (not accessControlAllowOriginList)
allowedOriginPatterns:
- "https://*.fndo.me"
- "https://fndo.me"
- "https://api.fndo.me"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
- PATCH
allowedHeaders:
- "*"
allowCredentials: false
maxAge: 86400Apply to IngressRoute:
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: api
spec:
entryPoints:
- websecure
routes:
- match: Host(`api.fndo.me`)
kind: Rule
services:
- name: api-service
port: 8080
middlewares:
- name: cors-dynamicDocker Compose Labels:
services:
api:
image: myapi:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
- "traefik.http.routers.api.middlewares=cors-dynamic"
- "traefik.http.middlewares.cors-dynamic.plugin.cors-dynamic-subdomain.allowedOriginPatterns=https://*.example.com,https://example.com"
- "traefik.http.middlewares.cors-dynamic.plugin.cors-dynamic-subdomain.allowedMethods=GET,POST,PUT,DELETE,OPTIONS"
- "traefik.http.middlewares.cors-dynamic.plugin.cors-dynamic-subdomain.allowCredentials=false"π Important: This plugin uses different parameter names than Traefik's built-in CORS:
| Traefik Built-in CORS | This Plugin | Purpose |
|---|---|---|
accessControlAllowOriginList |
allowedOriginPatterns |
Origin matching (this plugin supports wildcards!) |
accessControlAllowMethods |
allowedMethods |
HTTP methods |
accessControlAllowHeaders |
allowedHeaders |
Request headers |
accessControlAllowCredentials |
allowCredentials |
Cookie/auth support |
| Option | Type | Default | Description |
|---|---|---|---|
allowedOriginPatterns |
[]string |
(required) | List of origin patterns. Use * for wildcard subdomain matching. |
allowedMethods |
[]string |
["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"] |
HTTP methods to allow |
allowedHeaders |
[]string |
["*"] |
Headers to allow. Use ["*"] for all headers. |
exposedHeaders |
[]string |
[] |
Headers to expose to the browser |
allowCredentials |
bool |
false |
Whether to allow credentials (cookies, authorization headers) |
maxAge |
int |
86400 |
How long (in seconds) the preflight response can be cached |
allowedOriginPatterns:
# Match all subdomains
- "https://*.example.com" # β
https://shop1.example.com
# β
https://shop2.example.com
# β https://example.com (no subdomain)
# Match exact domain
- "https://example.com" # β
https://example.com
# β https://www.example.com
# Match with port
- "https://*.example.com:8080" # β
https://shop.example.com:8080
# β https://shop.example.com
# Multiple TLDs
- "https://*.example.com"
- "https://*.example.io"
- "https://*.example.dev"- Request arrives with
Origin: https://shop1.example.com - Plugin checks if origin matches any pattern in
allowedOriginPatterns - If match found, set response header:
Access-Control-Allow-Origin: https://shop1.example.com - Browser accepts because it received its exact origin back
Request:
βββββββββββββββββββββββββββββββββββββββ
β GET /api/products β
β Host: api.example.com β
β Origin: https://shop1.example.com β β Browser sends origin
βββββββββββββββββββββββββββββββββββββββ
β
[Traefik + Plugin]
β Matches pattern: https://*.example.com
β
Response:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Access-Control-Allow-Origin: https://shop1.example.com β β Exact origin
β Access-Control-Allow-Methods: GET, POST, ... β
β Access-Control-Allow-Headers: * β
β Vary: Origin β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
If you need to send cookies or authorization headers:
allowCredentials: trueallowCredentials: true, you cannot use * in allowedOriginPatterns. All origins must be explicitly listed or use specific wildcard patterns.
Frontend code:
fetch('https://api.example.com/data', {
credentials: 'include', // β Required for cookies
headers: {
'Authorization': 'Bearer token'
}
});cd cors-dynamic-subdomain
go test -v# Test subdomain
curl -i https://api.fndo.me/test \
-H "Origin: https://bigrob.fndo.me"
# Should return:
# Access-Control-Allow-Origin: https://bigrob.fndo.me
# Test preflight
curl -i https://api.fndo.me/test \
-X OPTIONS \
-H "Origin: https://shop.fndo.me" \
-H "Access-Control-Request-Method: POST"| Feature | Built-in CORS | This Plugin |
|---|---|---|
| Exact origin matching | β | β |
Wildcard * |
β | β |
Subdomain wildcard *.example.com |
β Returns literal string | β Returns exact origin |
| Multiple patterns | β | β |
| Dynamic origin response | β | β |
| Configuration key | accessControlAllowOriginList |
allowedOriginPatterns |
Check:
- Origin matches pattern exactly (including protocol and port)
- Plugin is applied to the correct route
- No other middleware is removing CORS headers
Ensure:
allowCredentials: trueAnd frontend uses:
credentials: 'include'Pattern: https://*.example.com
- β
Matches:
https://shop.example.com - β Does NOT match:
https://example.com(no subdomain) - β Does NOT match:
http://shop.example.com(different protocol)
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new features
- Submit a pull request
MIT License - see LICENSE file
Built for developers who need Shopify-like multi-tenant architectures with Traefik.
Created by x-ream