Skip to content

Answer browser preflights at the gateway and control which web origins may call your API.

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 (plus Access-Control-Expose-Headers and, when configured, Access-Control-Allow-Credentials), with Vary: Origin where caching requires it.
  • A preflight — OPTIONS with Access-Control-Request-Method — is answered directly with 204 No Content and 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 cors policy fields are not supported and are refused at startup; there is no typed_per_filter_config override for CORS. Use separate filter chains or listeners for routes that need different policies.
  • filter_enabled / shadow_enabled runtime fractions are not supported.
  • allow_private_network_access and forward_not_matching_preflights are 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.

Continue

Start typing to search the site.

↑ ↓ to move Enter to open Esc to close