How to configure CORS for Orchestration Cluster REST API in Camunda 8.8 (c8run)?

My questions:

  1. Does Camunda 8.8 support configuring CORS for the Orchestration Cluster REST API?

  2. If yes, what is the recommended way to enable CORS when running with c8run?

  3. Are there specific properties that need to be set?

Any example configuration would be greatly appreciated.

Camunda 8.8’s Orchestration Cluster exposes cross-origin HTTP header properties (COEP/COOP/CORP) that can be set via application.yaml when running c8run, though these differ from classic CORS Access-Control-Allow-* headers. I found the following relevant resources:

Does this help? If not, can anyone from the community jump in? :waving_hand:


:light_bulb: Hints: Use the Ask AI feature in Camunda’s documentation to chat with AI and get fast help. Report bugs and features in Camuda’s GitHub issue tracker. Trust the process. :robot:

Hi @prabir , Camunda 8.8 does not expose a first-class “classic CORS” allow-list property (i.e. there is no Access-Control-Allow-Origin / allowed-origins setting) for the Orchestration Cluster REST API (/v2 on the Zeebe Gateway). What it does expose is the family of cross-origin isolation headers (COEP / COOP / CORP) plus a pluggable REST filter mechanism. For real browser CORS you either add a custom servlet filter to the gateway or (recommended) terminate CORS at a reverse proxy in front of Camunda.


1. Does 8.8 support configuring CORS for the Orchestration Cluster REST API?

  • Not as classic CORS. There is no built-in property that emits Access-Control-Allow-Origin, Access-Control-Allow-Methods, etc. for the /v2 REST API.
  • The Orchestration Cluster does let you configure the cross-origin HTTP headers COEP/COOP/CORP via camunda.security.http-headers.*. These control browser embedding/isolation — they are not the same as CORS Access-Control-Allow-* headers, so setting them will not by itself fix a browser CORS preflight failure.
  • The gateway REST server is a Spring Boot application, and it supports a documented REST filter extension point (camunda.api.rest.filters) that you can use to inject a jakarta.servlet.Filter implementing CORS.

2. Recommended way to enable CORS with c8run

Option A — Reverse proxy (recommended for anything beyond local dev). This is Camunda’s standard recommendation: put a reverse proxy (nginx, Apache, Spring Cloud Gateway, an API gateway, etc.) in front of the cluster and let it add the Access-Control-Allow-* headers and answer OPTIONS preflight. This keeps the Camunda API itself off the public internet.

Option B — Custom REST filter loaded into the gateway. The gateway supports loading arbitrary Jakarta servlet filters that apply only to the Orchestration Cluster REST API (they do not affect gRPC). You compile a small CORS filter into a JAR and reference it from application.yaml. Since c8run runs the standalone/embedded gateway, this is the in-process way to get CORS headers on /v2.

For c8run, put your settings in an application.yaml and start with:

# macOS / Linux
./start.sh --config application.yaml

# Windows
c8run.exe start --config application.yaml

(If changes don’t take effect, pass --config explicitly and fully restart — c8run only reliably picks up custom config this way.)

3. Specific properties

Cross-origin isolation headers (COEP/COOP/CORP) — available, but not classic CORS:

Property Header Options Default
camunda.security.http-headers.cross-origin-embedder-policy.value Cross-Origin-Embedder-Policy REQUIRE_CORP, UNSAFE_NONE UNSAFE_NONE
camunda.security.http-headers.cross-origin-opener-policy.value Cross-Origin-Opener-Policy UNSAFE_NONE, SAME_ORIGIN_ALLOW_POPUPS, SAME_ORIGIN SAME_ORIGIN_ALLOW_POPUPS
camunda.security.http-headers.cross-origin-resource-policy.value Cross-Origin-Resource-Policy SAME_ORIGIN, SAME_SITE, CROSS_ORIGIN SAME_SITE

Equivalent env vars: CAMUNDA_SECURITY_HTTPHEADERS_CROSSORIGINEMBEDDERPOLICY_VALUE, ..._CROSSORIGINOPENERPOLICY_VALUE, ..._CROSSORIGINRESOURCEPOLICY_VALUE.

Custom REST filter (the real CORS lever):

Property Description
camunda.api.rest.filters[].id Unique id for the filter
camunda.api.rest.filters[].jar-path Path to the JAR containing your filter class
camunda.api.rest.filters[].class-name Fully-qualified class name of your jakarta.servlet.Filter

Env-var form: CAMUNDA_API_REST_FILTERS_0_ID, CAMUNDA_API_REST_FILTERS_0_JARPATH, CAMUNDA_API_REST_FILTERS_0_CLASSNAME.

4. Example configuration

(a) c8run application.yaml setting the cross-origin isolation headers (so a browser can embed/load resources cross-origin):

camunda:
  security:
    http-headers:
      cross-origin-embedder-policy:
        value: UNSAFE_NONE
      cross-origin-opener-policy:
        value: SAME_ORIGIN_ALLOW_POPUPS
      cross-origin-resource-policy:
        value: CROSS_ORIGIN

(b) c8run application.yaml wiring a custom CORS servlet filter into the gateway REST API:

camunda:
  api:
    rest:
      filters:
        - id: cors-filter
          jar-path: /opt/camunda/filters/cors-filter.jar
          class-name: io.example.CorsFilter

Minimal filter implementation (compile against jakarta.servlet-api, JDK 21 or lower, package as cors-filter.jar):

package io.example;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public final class CorsFilter implements Filter {
  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    HttpServletResponse http = (HttpServletResponse) res;
    HttpServletRequest request = (HttpServletRequest) req;

    // Reflect/allow only the origins you trust
    http.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
    http.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
    http.setHeader("Access-Control-Allow-Headers", "Authorization,Content-Type,Origin,Accept");
    http.setHeader("Access-Control-Allow-Credentials", "true");
    http.setHeader("Access-Control-Max-Age", "3600");

    // Short-circuit preflight
    if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
      http.setStatus(HttpServletResponse.SC_NO_CONTENT);
      return;
    }
    chain.doFilter(req, res);
  }
}

Compile example:

javac -classpath lib/jakarta.servlet-api.jar CorsFilter.java
jar cf cors-filter.jar io/example/CorsFilter.class

(c) Reverse proxy alternative (nginx sketch) — the approach Camunda generally recommends:

location /v2/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin "http://localhost:3000";
        add_header Access-Control-Allow-Methods "GET,POST,PUT,PATCH,DELETE,OPTIONS";
        add_header Access-Control-Allow-Headers "Authorization,Content-Type,Origin,Accept";
        add_header Access-Control-Max-Age 3600;
        return 204;
    }
    add_header Access-Control-Allow-Origin "http://localhost:3000" always;
    proxy_pass http://localhost:8080;
}

Important caveats / notes

  • COEP/COOP/CORP ≠ CORS. If your issue is a browser blocking fetch/XHR from a different origin with a CORS error, the http-headers properties alone will not solve it — you need the servlet filter (Option B) or the proxy (Option A).
  • SPRING_MVC_CORS_MAPPINGS_* does not apply here. Those spring.mvc.cors.* mappings work for the classic Management Identity Spring MVC app, but the Orchestration Cluster gateway REST server is a different (reactive) Spring Boot server, so those MVC CORS mappings are not the mechanism for the /v2 API.
  • Best practice: don’t expose the Orchestration Cluster REST API directly to browsers/end users. Front it with your own backend/API layer or reverse proxy and enable authentication (camunda.security.authentication.unprotected-api: false and camunda.security.authorizations.enabled: true) — CORS is then handled at that edge.