---
title: "CORS"
url: "/docs/security/cors/"
kind: "feature"
summary: "Answer browser preflights at the gateway and control which web origins may call your API."
---

> Arion site index: [/llms.txt](/llms.txt)

# CORS

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:

```yaml,name=arion.yaml,arion=fragment
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](@/docs/security/jwt.md) 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](@/docs/mcp-gateway/overview.md).

## Permissive default

For development, the bare `Cors` config allows every origin with common methods, all headers and a 24-hour preflight cache:

```yaml,arion=fragment
- 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:

```yaml,arion=fragment
expose_headers: "x-request-id, x-trace-id"
```

## Verify

Simulate a preflight:

```bash
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](@/docs/security/jwt.md) or [RBAC](@/docs/security/rbac.md) to actually deny callers.
