Apache APISIX CORS configuration

Set cross-origin rules once at the gateway instead of in every service, with settings that hold up for authenticated APIs and browser gRPC-Web clients.

On this page

When APISIX CORS configuration replaces per-service CORS code

APISIX CORS configuration moves origin, method, header and credential rules into the cors plugin on the gateway, so each browser-facing service no longer has to answer preflight requests and set Access-Control headers on its own. The typical starting point is a team with several browser front ends calling services that each set their own CORS headers, often with slightly different allow lists, so a change to one origin means a change in every codebase. When the cors plugin is attached to a route or applied globally, APISIX answers preflight OPTIONS requests and adds the Access-Control-* response headers based on the plugin settings, such as allow_origins, allow_methods, allow_headers, expose_headers and allow_credentials. A team should consider centralizing when several services share the same front ends, when origin lists change more often than service code ships, or when a gateway already fronts the traffic.

Centralizing does not move every decision to the gateway. Before you remove CORS code from a service, confirm that the gateway is the only path to it, because a service reachable directly on the network would no longer receive the same headers. Also confirm how your services respond to requests that the gateway forwards without an Origin header, and test the exact headers your front ends read, since APISIX 3.10.0 stopped setting a default * for expose_headers. Review the settings for the version you run in the APISIX changelog.

CORS is a browser rule for which pages may read a response. It is not an access control. Whether a caller may perform an action, and which records it may see, still belongs in the service or in an authentication and authorization layer behind the gateway. The cors plugin does not check user roles, tokens or data ownership, so a request that passes CORS can still be refused or allowed by the service's own logic. Keep those checks in the service, and treat the gateway's CORS rules as a way to manage browser access in one place, not as a security boundary you can rely on alone.

Which cors plugin options to set and when each one fits

This table compares the main cors plugin settings an APISIX team will touch, when each one fits and what breaks when it is set too broadly. Read each row against the cors plugin reference for your APISIX version, because defaults changed in 3.10 and parameter names should be confirmed before you copy them into a route or plugin config.

SettingWhen it fitsWhat breaks if it is too broad
allow_origins (exact list)Browser apps served from a short, known set of front-end domains, such as one production and one staging siteAny unlisted origin is blocked, so new front ends fail until someone adds them. Keep the list under change control so additions are reviewed.
allow_origins (pattern)Several controlled subdomains that share a naming pattern, where listing each one would be error-proneA loose pattern can match attacker-controlled domains, so anchor it tightly and test it with look-alike hostnames.
allow_origins (wildcard *)Public, unauthenticated read-only APIs where any site may call the endpoint and no cookies or credentials are involvedAny site can read responses from the API in a browser. Do not use it on routes that rely on cookies, session tokens or client certificates.
allow_methodsLimit to the verbs the front end actually calls, such as GET, POST and OPTIONS for a typical read and write APIAllowing DELETE, PUT or PATCH to every origin lets cross-site pages invoke state-changing operations if the service has no other check.
allow_headersOnly the custom request headers the client sends, such as Authorization or a tenant or trace headerBroad header lists widen what a cross-site request can carry. Authorization is only sent when the client sets it, so confirm which headers each client needs.
expose_headersResponse headers the browser code must read, such as pagination or rate limit headers. Since 3.10 this is sent only when configuredExposing internal or sensitive response headers gives cross-site scripts access to values the client should not see. Expose the minimum set.
allow_credentialAuthenticated APIs where the browser must send cookies or other credentials across originsEnabling it together with a wildcard origin is unsafe and is not a configuration to ship. Pair it only with an exact origin list.
max_age (preflight cache)Stable APIs where browsers can cache preflight results for a fixed period, which reduces OPTIONS trafficA long cache keeps old rules in the browser after you tighten the policy. Shorten it before a planned change and confirm the default for your version.
Vary: Origin header (since 3.3)Any deployment where allow_origins is not the wildcard, so caches and proxies do not reuse a response built for another originWithout Vary, a shared cache can serve one origin's CORS headers to another. Confirm that upstream caches and CDNs also respect it.

What changed in the cors plugin at 3.3 and 3.10

Two changes to the APISIX cors plugin affect existing configurations, and both can break behavior that looked fine before the upgrade. Since APISIX 3.3.0, the plugin adds a Vary: Origin response header whenever allow_origin is not set to *. The header tells caches that the response depends on the request's Origin, so a shared cache does not serve the response for one origin to a browser from another origin. If you run a CDN, a reverse proxy cache or any shared cache in front of the gateway, confirm that Vary: Origin survives every hop to the client. Some caching layers strip or rewrite Vary by default, and a stripped header can produce intermittent CORS failures that only appear for some users.

Since APISIX 3.10.0, the plugin no longer sets a default * for expose_headers. It sends Access-Control-Expose-Headers only when you configure expose_headers explicitly. Teams that relied on the old default, where browser scripts could read every response header, will find that custom headers such as pagination or request ID fields are no longer visible to JavaScript. Before you upgrade, search your frontend code and API documentation for response headers that clients read, then list them in expose_headers on the affected routes or global rules. Test this on a non-production route, because the failure shows up in the browser console rather than in the gateway logs.

Both changes are recorded in the apache/apisix CHANGELOG. Read the entries between your current version and your target version before planning the upgrade, since other cors behavior or related plugin changes may also apply to your environment. Confirm the exact release entries in the changelog rather than relying on this summary alone.

How to set up gateway CORS in a staged rollout

This sequence moves CORS handling from individual services to the APISIX gateway in a controlled order, so each browser origin is confirmed before production traffic depends on the new policy. It ends with a single source of CORS headers and a repeatable path for future changes.

  1. Inventory browser origins and request needs

    The application owner and the platform team list every browser origin that calls each API, including staging and partner domains. For each origin, record the HTTP methods, custom request headers (such as Authorization or a tenant header), whether the browser sends cookies or other credentials, and any response headers the front end reads. This list becomes the allowlist for the cors plugin. The step is done when every origin has an owner and every credentialed origin is marked, since wildcard origins cannot be used with credentials. Confirm the list with the front-end team, not only from code search.

  2. Create the test route with exact origins

    In a non-production APISIX environment, create a route for one API and attach the cors plugin with allow_origins set to the exact scheme, host and port of each approved origin, not *. Only set allow_credentials to true for origins that need it, and set allow_headers and allow_methods to the values from the inventory. Apply the change through the same configuration path your team uses for routes. The step is done when the route returns the configured policy on a test request. Check the plugin reference for the current option names in your APISIX version.

  3. Test simple requests from an approved origin

    Send a simple GET request from an allowed origin using curl with an Origin header, for example curl -i -H "Origin: Confirm that the response contains Access-Control-Allow-Origin` with the requesting origin and no more than one copy of the header. Then repeat the request with an origin that is not on the list and confirm the header is absent. The step is done when both results match the inventory. Run the same check in browser developer tools on the real front end to see what the browser accepts.

  4. Test preflight requests separately

    Send an OPTIONS request with Access-Control-Request-Method and Access-Control-Request-Headers set to the values a browser would send, for example `curl -i -X OPTIONS -H "Origin: -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: authorization,content-type" Confirm that the preflight response lists the allowed methods and headers and that the status code is success. Also test a method or header not on the list and confirm it is refused. For gRPC-Web routes, check preflight behavior against the grpc-web plugin documentation, since that plugin handles OPTIONS requests itself and allows all origins by default unless you configure it. The step is done when preflight results match the inventory for every method and header.

  5. Confirm Vary and expose headers

    Check that responses for origins other than * include Vary: Origin, which APISIX adds from version 3.3.0 when allow_origin is not *. This tells caches that the response depends on the requesting origin. Then confirm Access-Control-Expose-Headers is present only if you configured expose_headers, since from version 3.10.0 APISIX no longer sends a default * value. Use curl -i on a simple request and look at the headers, and check that any custom response header your front end reads, such as a pagination or request ID header, appears in the expose list. The step is done when the headers match your configuration and no unexpected wildcard values appear.

  6. Remove CORS headers from upstream services

    In each upstream service, disable or remove its own CORS middleware, library, or reverse proxy headers so the gateway is the only source of CORS headers. Then send the same requests through the test route and compare the responses with those from a direct call to the service inside the cluster or network. Look for duplicate Access-Control-Allow-Origin headers, which browsers reject. The step is done when a request through the gateway returns exactly one set of CORS headers and the service's direct responses have none. Coordinate this with the service teams, because a service that still sends its own headers can override the policy.

  7. Promote the policy through the release path

    Move the same route and cors configuration into staging and then production using the process your team already uses for route changes, such as version control and review. Repeat the curl checks from the earlier steps in each environment and keep the outputs with the change record. Record the APISIX version in use, because header behavior changed in 3.3.0 and 3.10.0. Add an origin review to the process so new front-end domains go through the same inventory and test steps before they are added. The step is done when production passes the same checks and the approved origin list has a named owner.

Choosing CORS settings for public, authenticated and server-to-server APIs

This case covers APIs that return public data and where browsers on other origins call them without cookies, client certificates or bearer tokens. Because no credentials cross origins, a wildcard allowed origin does not expose user data, which is the only situation where it is acceptable. The cors plugin in APISIX handles the preflight and response headers so individual services do not need to.

  • **allow_origins set to *:** Acceptable here because the response carries no user-specific data and the browser will not attach credentials.
  • allow_credentials left off: Keep it disabled, since enabling credentials with a wildcard origin is not a valid combination for browsers.
  • allowed methods and headers: Limit methods to GET and OPTIONS and allow only the request headers the client actually sends.
  • expose_headers: Since APISIX 3.10.0 the plugin sends Access-Control-Expose-Headers only when you configure it, so list only the response headers the client must read.
  • max_age: Set a preflight cache duration that suits your change rate, and confirm with your team how often the policy will change.

Before go-live, confirm that the API response contains no personalized data, that no route accepts cookies or tokens for this path, and that a preflight request from an unlisted origin behaves as your policy intends.

Handling gRPC-Web preflight with the grpc-web plugin

The grpc-web plugin in APISIX translates gRPC-Web requests from browsers into native gRPC calls, accepting the application/grpc-web and grpc-web-text content types. Out of the box it allows all origins and answers OPTIONS preflight requests itself. That default is convenient for a public demo, but it is the wrong starting point for a route that carries authenticated calls. Before you go live, decide which origins may call each gRPC-Web route and set that explicitly, rather than accepting the permissive default and tightening it later.

Keeping the default on an authenticated route has a practical cost. Any site a user visits can send cross-origin requests to the gRPC-Web endpoint, and the browser will attach the user's cookies or tokens if the client is configured to do so. Restrict the allowed origins to the front-end hosts you control, and confirm that the preflight response returns only the headers your client needs. Review the grpc-web plugin documentation at the APISIX grpc-web plugin page to confirm the options available in your APISIX version before writing the configuration.

Pairing the grpc-web plugin with the cors plugin on the same route is possible, but the combined behavior needs testing, not assumption. Both plugins can answer preflight and set response headers, so check which one wins, whether the Access-Control-Allow-Origin value matches your intended origin list, and whether the exposed headers your gRPC-Web client reads are still present. Run these checks in a test environment with the same route shape as production. Also confirm protocol details such as the content types and trailer handling against your client library, since the gateway behavior has to match what that library sends and expects.

Which CORS mistakes expose authenticated APIs

Most CORS exposure on authenticated APIs comes from a small set of configuration choices made at the gateway. Each card below names one mistake, the control that prevents it in Apache APISIX, and a check you can run before and after a change.

Wildcard origin on credentialed routes

Setting allow_origin to * on a route that accepts cookies or bearer tokens lets any website script read responses from that API when a user is signed in. Browsers block credentialed wildcard responses in many cases, but the configuration still signals that the API has no origin boundary. Set an explicit list of trusted origins for every route that carries user credentials, and reserve * for truly public, unauthenticated endpoints. Verify by sending a preflight request from an origin outside the list and confirming that no matching Access-Control-Allow-Origin header comes back.

Reflecting any request origin

Some configurations copy the incoming Origin header into the response without checking it against a list. The effect is the same as a wildcard, but it also works with credentials, so any site a signed-in user visits can call the API. Keep an allow list of exact origins, including scheme and port, and avoid patterns that match suffixes such as a shared hosting domain. Verify with a request from an unlisted origin and confirm that the response does not echo that origin back.

Missing Vary: Origin behind a cache

When the allowed origin differs by request, a shared cache can store a response built for one origin and serve it to another. Since APISIX 3.3.0, the cors plugin adds Vary: Origin automatically when allow_origin is not *. Check that the header reaches the client and that your CDN or reverse proxy honors it, including any cache rules that strip Vary. Verify by requesting the same URL with two different origins through the cache and confirming that each receives its own CORS headers.

Broad defaults kept after upgrade

Since APISIX 3.10.0, the cors plugin no longer sets a default * for expose_headers, so the header is sent only when configured. Teams that relied on the older behavior, or that copied configuration from examples written before the change, may now expose fewer headers than their clients expect. Review every route that uses cors during an upgrade, and set expose_headers explicitly to the response headers the browser client needs. Verify by checking the Access-Control-Expose-Headers header on a real response before and after the upgrade.

Duplicate CORS headers from gateway and service

If both APISIX and the upstream service add Access-Control-Allow-Origin, browsers can reject the response because the header appears more than once or the two values disagree. Pick one owner for CORS, usually the gateway, and remove the CORS middleware from services that sit behind it. Confirm the owner in every environment, since a service may still add headers in one path only. Verify by inspecting raw responses from each route and counting the CORS headers returned.

Preflight bypassing authentication

Browsers send an OPTIONS preflight without credentials before the real request. If the gateway answers preflight before authentication runs, that is expected for CORS, but the preflight must not reveal protected data or allow methods and headers that the real route would refuse. For gRPC-Web, the grpc-web plugin handles OPTIONS and by default allows all origins, so restrict its origin settings and confirm the authenticated route still rejects unauthenticated calls. Verify by sending a preflight and a real request without a token and confirming that the real request returns an authentication failure.

What teams ask before they hire help with APISIX CORS configuration

How much effort does moving CORS to the gateway take?

The effort depends mostly on how many origins, services and routes you have, and how much CORS logic already lives in application code. Teams with a few origins and one service usually need a short change set. Larger estates take longer because each route needs its own origin list, credential setting and preflight check, and each existing service handler must be reviewed before its CORS code is removed. Other drivers are whether browsers send credentials, whether any origin is a wildcard today, and whether gRPC-Web traffic is involved, since the grpc-web plugin handles preflight in its own way. Ask for an origin inventory and a route count before comparing quotes.

Can our team configure APISIX CORS alone using the docs?

Yes, many teams can, provided they have a staging environment, someone who can use the Admin API, and a clear list of the browser clients that call each API. The official documentation covers the cors plugin and the grpc-web plugin, and the APISIX changelog records behavior changes such as the Vary header and expose_headers defaults. The harder part is testing: you need to confirm preflight responses, credentialed requests and error responses from each origin, and to check that no service still sends conflicting headers. If that testing is not planned, a partner can run it under a written scope.

What do we need to provide before CORS work starts?

Most of the input is information you already hold or can collect quickly:

  • An inventory of browser origins, including staging, partner and internal portal domains
  • A list of routes and upstream services that receive browser traffic
  • Test accounts for each authentication path, so credentialed requests can be checked
  • Staging access to the APISIX Admin API, with a role that can change routes and plugin configuration
  • Current CORS headers from each service, captured from a browser or with curl

Without the origin list and test accounts, the testing window usually grows.

Does gateway CORS remove the need for CORS in our services?

It reduces the need but rarely removes it completely. The gateway can set the CORS headers for routes that pass through APISIX, so individual services no longer have to repeat that code. However, services may still need their own CORS handling for paths that bypass the gateway, for internal callers that reach them directly, or for specific responses the gateway does not rewrite. Keep the gateway as the single source for browser-facing routes, then review each service to decide what to remove and what to keep.

How does the Vary header change affect caching?

Since APISIX 3.3.0, the cors plugin adds Vary: Origin when allow_origin is not set to a wildcard. This tells caches that the response can differ by requesting origin, so a shared cache should store separate entries for each origin rather than serving one origin's headers to another. If you run a CDN, a proxy cache or the proxy-cache plugin in front of APISIX, confirm that the cache honors Vary and that your cache key configuration matches the change. Test with two origins before rollout to see whether hit rates shift.

How is the cost structured in a fixed-scope quote?

A fixed-scope quote usually lists the work as separate, written deliverables, such as an origin and route inventory, the plugin configuration per route, preflight and credential tests, gRPC-Web checks where relevant, and handover notes. Costs are one-time for the configuration and testing. Recurring costs appear only if you add a retainer for ongoing support or upgrades, and you should ask for that separately. Ask the quote to state what is excluded, which environments are covered, how changes after sign-off are priced, and what evidence you receive. DeployOpen does not publish a price list, and the quote follows from the written scope.

Which CORS settings should we never use on authenticated APIs?

Avoid a wildcard origin on any API that accepts credentials, such as cookies or authorization headers. Browsers will not accept a wildcard with credentials, and a permissive origin list invites cross-origin reads of authenticated data. List exact origins, enable credentials only for routes that need them, and keep expose_headers limited to headers the client actually reads. Since APISIX 3.10.0 the cors plugin no longer sets a default wildcard for expose_headers, so confirm each route's configuration after upgrading. Ask your security reviewer to check the origin list before go-live.

What to confirm in your APISIX version and environment

CORS behavior at the gateway depends on the APISIX release you run, the path traffic takes to browsers, and what each upstream service already returns. The cards below list the points to verify in your own environment before you move CORS out of the services.

Exact cors plugin behavior by release

The cors plugin changed in 3.3.0 (Vary handling) and in 3.10.0 (expose_headers no longer defaults to *). Read the changelog entries between your running version and the target version, then test the headers your browsers actually receive. Do not assume a behavior carried over from an older example in a blog post or a previous environment.

Vary header survival across the network path

Since 3.3.0, APISIX adds Vary: Origin when allow_origin is not *, so caches can key responses per origin. Confirm that your CDN, reverse proxy and load balancers keep this header, do not strip it, and do not cache a response for one origin and serve it to another. Test each hop with the origins your clients use.

Duplicate CORS headers from upstream services

If an upstream service also sets Access-Control-Allow-Origin or related headers, browsers may receive conflicting or duplicated values. Inventory every service behind the gateway, note which ones emit CORS headers, and decide which layer owns them. Check the responses on both direct and gateway paths, including error responses from the upstream.

Plugin order for preflight requests

Preflight OPTIONS requests carry no credentials, so an authentication plugin that runs before cors can reject them and break the browser flow. Review the plugin order on each route, global rule and plugin config, and check how plugin precedence applies in your setup. Send a preflight request without a token and confirm that it returns the expected CORS headers.

Credentials and origins per environment

Confirm whether browser clients send cookies or authorization headers with credentials, and list the exact origins each environment uses, such as staging and production hostnames. Credentialed requests need explicit origins rather than a wildcard, so each allowed origin should be a deliberate entry in the route or plugin configuration. Record the list under version control.

gRPC-Web headers and preflight handling

The grpc-web plugin accepts application/grpc-web and grpc-web-text content types and handles OPTIONS preflight by default. Check which custom headers your gRPC-Web client library sends, such as x-grpc-web or x-user-agent, and confirm that the cors settings allow them. Test with the exact client library version your frontend team ships.

Primary sources for APISIX CORS and gRPC-Web behavior

Before you change CORS settings on a gateway, check the apache/apisix CHANGELOG for the cors plugin. Two entries matter most for this topic: the change in 3.3.0, where the plugin began adding Vary: Origin when allow_origin is not *, and the change in 3.10.0, where the plugin stopped setting a default * for expose_headers and sends Access-Control-Expose-Headers only when you configure it. Those two changes can alter what browsers receive after an upgrade, so the changelog answers the question of whether your existing responses will look different. Read it from the version you run to the version you plan to move to, and link the entries to your upgrade ticket. The changelog is kept in the apache/apisix repository on GitHub, which is the natural starting point for this review.

The APISIX grpc-web plugin documentation answers a different question: how a browser's gRPC-Web calls are translated into native gRPC, which content types it accepts, and how it handles OPTIONS preflight. The documentation states that the plugin accepts application/grpc-web and grpc-web-text content types and that, by default, it allows all origins and handles preflight requests itself. If you rely on the plugin for browser clients, confirm whether the default origin policy is acceptable for your environment, and whether your cors configuration and the grpc-web behavior agree on which origins and headers are permitted. Test both with a real browser origin, not only with a command-line request.

Match every page you read to the APISIX version you actually deploy. The changelog entries and plugin documentation change between releases, and the documentation site may show a newer default than the one running in your cluster. Record the deployed version in your change record, pull the matching documentation for that version, and rerun your preflight and credentialed request tests after each upgrade. If a behavior you need is not described in the documentation for your version, ask the project or confirm it in a test environment before you depend on it.

How DeployOpen scopes and delivers gateway CORS work

DeployOpen treats CORS on Apache APISIX as a gateway policy that is scoped in writing, built route by route, tested against real browser origins and handed over with the configuration and runbooks your team needs. Engagements are fixed-scope, so the origins, routes and credential rules to be covered are agreed before any configuration changes.

How we scope

  • We list every browser origin, route and service that needs CORS at the gateway before any change.
  • We record which routes carry credentials, cookies or Authorization headers, since those set the strictest origin rules.
  • We agree in writing whether gRPC-Web routes, the grpc-web plugin and preflight handling are in scope.

How we build

  • We configure the cors plugin per route or shared plugin config, with explicit allowed origins instead of wildcards on authenticated APIs.
  • We check the APISIX version in your environment, since Vary: Origin and expose_headers behavior depend on the release.
  • We test preflight OPTIONS requests and response headers from each allowed and disallowed origin before cutover.

How we hand over

  • You receive the final plugin configuration, the origin allowlist and the rationale for each credentials decision.
  • We document how to add an origin, review a change and verify headers after an APISIX upgrade.
  • Test scripts for preflight and credentialed requests are handed over so your team can rerun them.

How we support

  • Ongoing support, upgrades and continuous testing of gateway policy are available as retainers.
  • We can re-run the origin and header test suite when APISIX versions, routes or identity providers change.
  • Support scope for CORS changes is confirmed in writing alongside the original quote.

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.