Configuring CORS and HTTP Security Headers
This guide walks through common CORS and HTTP security header configurations for different deployment scenarios.
Background
When a browser-based application (SPA, mobile web, etc.) makes requests to Bondy from a different origin, the browser enforces the CORS policy. Without the right CORS headers, the browser blocks the response.
Bondy configures CORS and security headers at the listener level in bondy.conf, under listeners.<name>.cors.* and listeners.<name>.security_headers.*. Every endpoint served by that listener (API Gateway routes, SSE/long-poll WAMP transports, OIDC login/callback, OAuth2 token endpoints) inherits the same CORS policy. The examples below use listeners named public_http and public_https — substitute the names your listener inventory declares.
INFO
See the HTTP Security Headers Reference for the complete list of configuration keys.
Scenario 1: Development (allow everything)
During development you typically want browsers to allow all cross-origin requests without friction. This is the default configuration:
listeners.public_http.cors.enabled = on
listeners.public_http.cors.allowed_origins = *WARNING
Wildcard origin forces Access-Control-Allow-Credentials to false. If your frontend needs to send cookies or Authorization headers, switch to an explicit allowlist or auto mode.
Scenario 2: Single frontend origin
The most common production setup — one SPA served from a known domain:
listeners.public_https.cors.allowed_origins = https://app.example.comWith this configuration:
- Requests with
Origin: https://app.example.comreceiveAccess-Control-Allow-Origin: https://app.example.comandAccess-Control-Allow-Credentials: true - Requests from any other origin (or no origin) receive no CORS headers, causing the browser to block them
- A
Vary: Originheader is added so caches distinguish between origins
Scenario 3: Multiple frontend origins
When several frontends share the same Bondy cluster:
listeners.public_https.cors.allowed_origins = https://app.example.com, https://admin.example.com, https://partner.example.comThe incoming Origin header is matched against this list. Only exact matches are reflected back.
Scenario 4: Wildcard subdomains
When you have many subdomains under the same base domain (e.g. per-tenant subdomains), use the *. prefix instead of listing every one:
listeners.public_https.cors.allowed_origins = *.example.comThis matches https://app.example.com, https://tenant1.example.com, etc. It does not match the bare domain https://example.com — add it explicitly if needed:
listeners.public_https.cors.allowed_origins = *.example.com, https://example.comYou can mix wildcard patterns with exact origins:
listeners.public_https.cors.allowed_origins = *.example.com, https://partner.other.comThe exact requesting origin is always reflected back in the Access-Control-Allow-Origin header. The *. pattern is purely a server-side config convenience.
Scenario 5: Same-origin (auto mode)
If your frontend is served from the same domain as Bondy (e.g. via a reverse proxy), you can derive the origin automatically from the request:
listeners.public_https.cors.allowed_origins = autoBondy constructs the origin from Scheme://Host[:Port] of the incoming request (default ports 80 and 443 are omitted). This is equivalent to "same-origin only".
Scenario 6: Per-endpoint overrides via API Gateway specs
When different API endpoints need different CORS policies, you can define CORS headers directly in the API Gateway specification using MOPS expressions:
{
"variables": {
"partner_cors": {
"access-control-allow-origin": "https://partner.example.com",
"access-control-allow-credentials": "true",
"access-control-allow-methods": "GET,POST",
"access-control-allow-headers": "content-type,authorization",
"access-control-max-age": "3600"
}
},
"versions": {
"1.0.0": {
"paths": {
"/partner/data": {
"get": {
"response": {
"on_result": {
"headers": "{{variables.partner_cors}}",
"body": "{{action.result}}"
}
}
}
}
}
}
}
}When an API spec defines an access-control-allow-origin header, it takes precedence over the listener-level configuration. Endpoints without spec-defined CORS headers fall back to the listener configuration.
Hardening security headers
Beyond CORS, Bondy can set standard security response headers on every response. These are configured per-listener and address common findings from security scanners.
Recommended production settings
## Enforce HTTPS via HSTS (HTTPS listener only)
listeners.public_https.security_headers.hsts = max-age=31536000; includeSubDomains
## Prevent clickjacking
listeners.public_https.security_headers.frame_options = DENY
## Prevent MIME sniffing
listeners.public_https.security_headers.content_type_options = nosniff
## Content Security Policy (tune to your application)
listeners.public_https.security_headers.content_security_policy = default-src 'self'; frame-ancestors 'none'
## Suppress server version disclosure
listeners.public_https.server_header = serverThe default server_header value, bondy, sends bondy/<version>. Bondy sends any other value as written, so a value without a version keeps the Bondy version out of responses. Do not leave the value empty. bondy.conf treats a key with no value as a syntax error, and the node does not start.
Disabling individual headers
Set a header to off to suppress it. The listener still sends the other security headers:
## No CSP (too complex for this deployment)
listeners.public_https.security_headers.content_security_policy = off
## No HSTS (TLS terminated at load balancer)
listeners.public_https.security_headers.hsts = offDisabling all security headers
listeners.public_http.security_headers.enabled = offVerifying your configuration
After configuring, verify with curl:
# Check CORS headers on a preflight request
curl -s -D - -o /dev/null \
-X OPTIONS \
-H "Origin: https://app.example.com" \
https://your-bondy-host:<port>/wamp/sse/open
# Check security headers on a regular request
curl -s -D - -o /dev/null \
https://your-bondy-host:<port>/pingReplace <port> with the port of the listener you configured. The first request needs a listener that serves wamp_sse. The second request needs a listener that serves admin.
Look for:
access-control-allow-originmatching your frontend origin (not*)access-control-allow-credentials: true(when using an allowlist)x-frame-options: DENYorSAMEORIGINx-content-type-options: nosniffstrict-transport-security(on HTTPS only)serverheader absent or generic