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