The CORS filter handles cross-origin browser traffic at the gateway: it answers OPTIONS preflights directly and stamps Access-Control-* headers on responses, so backends never need CORS logic. Use it whenever a web app on one origin calls an API served by Arion on another.
Enable it
Two typed configs drive the same filter. Cors enables enforcement with permissive defaults; CorsPolicy defines your own origins, methods and headers. To allow a web origin to call an API:
http_filters:
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy
allow_origin_string_match:
- exact: https://app.example.com
allow_methods: "GET, POST, OPTIONS"
allow_headers: "content-type, authorization"
allow_credentials: true
max_age: "600"
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
allow_methods, allow_headers and expose_headers are comma-separated strings; allow_origin_string_match takes string matchers (exact, prefix, suffix, contains, safe_regex), so suffix: .example.com covers subdomains.
Place the filter early in http_filters — in particular before JWT authentication and RBAC, because preflights carry no credentials and must be answered before authentication runs.
How enforcement works
Requests without an Origin header pass through untouched. For the rest:
- If the origin matches, responses gain
Access-Control-Allow-Origin(plusAccess-Control-Expose-Headersand, when configured,Access-Control-Allow-Credentials), withVary: Originwhere caching requires it. - A preflight —
OPTIONSwithAccess-Control-Request-Method— is answered directly with204 No Contentand the allow-lists, never reaching the backend, provided the requested method and headers are allowed. - An origin, method or header that is not allowed produces no CORS headers; the request is forwarded and the browser blocks the response. The gateway does not reject it.
Wildcards follow the spec: * in origins or headers is sent literally only while allow_credentials is false; with credentials enabled the matched origin is echoed back and * in allow_headers becomes a literal header name. The mcp-session-id header is always added to the allowed and exposed lists so browser-based MCP clients work through the MCP gateway.
Permissive default
For development, the bare Cors config allows every origin with common methods, all headers and a 24-hour preflight cache:
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
Swap it for a CorsPolicy before production.
Expose response headers
Browsers hide most response headers from JavaScript unless listed:
expose_headers: "x-request-id, x-trace-id"Verify
Simulate a preflight:
curl -i -X OPTIONS http://127.0.0.1:8080/api \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST"
Expected: HTTP/1.1 204 No Content with Access-Control-Allow-Origin: https://app.example.com and the configured Access-Control-Allow-Methods. Repeat with an unlisted origin and the CORS headers disappear.
Limits
- The policy attaches to the filter itself, chain-wide. Envoy's per-route and per-virtual-host
corspolicy fields are not supported and are refused at startup; there is notyped_per_filter_configoverride for CORS. Use separate filter chains or listeners for routes that need different policies. filter_enabled/shadow_enabledruntime fractions are not supported.allow_private_network_accessandforward_not_matching_preflightsare not supported; a failed preflight is forwarded upstream rather than rejected at the gateway.- CORS is browser policy, not authorization — pair it with JWT or RBAC to actually deny callers.