Skip to main content
Version: 5.x

DOMAIN_MAP

DOMAIN_MAP is a JSON object. Each key is a request authority, the Host a client sends. Each value tells the relay where to send matching requests and how to treat them. It is the only routing setting in v5; the v4 single upstream variables (UPSTREAM, CLIENT_ID_SECRET, CORS_ORIGINS, and the rest) are gone.

{
"api.example.com": {
"upstream": "http://api-service:8080",
"client_id_secret": "REPLACE_WITH_A_LONG_RANDOM_SECRET",
"cors_origins": ["https://app.example.com"],
"pass_through_routes": ["/health"]
}
}

Set it as an environment variable, or manage it from the control plane, where the Proxies page edits one host at a time. DOMAIN_MAP is a live setting: a control plane change reaches every worker within one poll interval without a restart.

Keys​

A key is one of:

Key formExample
Hostnameapi.example.com
Hostname with an explicit portlocalhost:3000
Bracketed IPv6 literal, with or without a port[::1], [::1]:3000
Subdomain wildcard*.example.com
Global wildcard*

Keys are case insensitive: API.example.com and api.example.com are the same key. A map that holds two keys that differ only by letter case is refused, because the relay could not tell which entry to use.

A key fails validation, and the relay refuses to start, when:

  • a wildcard carries a port (*.example.com:3000), or has more than one *, or has a * anywhere but the start;
  • a port is outside 1 to 65535 or has leading zeros;
  • an IPv6 literal is not bracketed, or carries a zone id;
  • it contains /, whitespace, control characters, or invisible characters;
  • the hostname is longer than 253 characters;
  • another key differs from it only by letter case.

SocketX reads the same key grammar and the same member names for the members both products have (upstream, client_id_secret, cors_origins, and preserve_host), and matches hosts with the same rules below, so a key selects the same requests on either product.

Host matching​

For each request the relay parses the Host header into a hostname and an optional explicit port, then tries these in order and uses the first match:

  1. The exact host and port, only when the request carried a port. localhost:3000 matches the key localhost:3000.
  2. The exact hostname. localhost:3000 and localhost both match the key localhost.
  3. Subdomain wildcards, most specific first. a.b.example.com tries *.b.example.com before *.example.com, so overlapping wildcards always resolve the same way.
  4. The global wildcard *.

A few consequences worth knowing:

  • No match is answered 480 no_domain_for_host, and the message names the Host the relay saw.
  • A Host that does not parse, such as an unbracketed IPv6 address or a bad port, is answered 490 invalid_request. It is never routed through *.
  • A request with no Host header can only match *.
  • A port is never inferred, so example.com does not match the key example.com:443.
  • Keys and the request Host compare case insensitively, so API.Example.com matches the key api.example.com. Write keys in lowercase, the form clients normally send.

Auth, pairing, keepalive, CORS preflights, pass through routes, and proxying all resolve a request against the same map snapshot, so one request can never match one entry while pairing and another while forwarding.

Fields​

FieldTypeDefaultDescription
upstreamstringrequiredAbsolute http:// or https:// URL with a host, checked at startup so a bad value stops the relay instead of failing every request. The request path and query are appended to it, so a base path in the URL is kept as a prefix. On an outbound domain it is the base URL of the next relay.
client_id_secretstringgeneratedHMAC key that signs the client tokens for this domain. When it is missing, each relay generates a random secret at startup and logs a warning that names the hosts, which means clients pair again after every restart and replicas cannot share clients. Set an explicit value, the same one on every replica, on anything but a single local process.
outboundboolfalseMakes this an outbound domain: plaintext requests are framed and sent to the next relay named by upstream.
personalization_stringstringnoneOutbound domains only. A value the relay appends to the personalization strings of the pairs it creates toward the next relay. Outer whitespace is trimmed.
allow_plain_upstreamboolfalseOutbound domains only. Accepts an http:// upstream. Without it, an outbound domain with an http:// upstream fails validation, because that hop carries the client token.
pass_through_routesstring[]nonePaths proxied as plain HTTP with no MTE: an exact path such as /health, or a prefix ending in * such as /public/*. Regular expressions are not supported. The relay's own /api/mte-relay, /api/mte-pair, /api/mte-keepalive, and /api/mte-echo endpoints are always served by the relay. With HEALTH_CHECK_PATH set, GET and HEAD at that path get the health answer before pass through routes as well, and other methods still reach the origin, so pick a path the origin does not use.
headersobjectnoneHeaders set on every upstream request, replacing a forwarded header of the same name. Names under the relay owned X-MTE-Relay- prefix are ignored. Host and hop by hop headers are dropped; use preserve_host to send the client's host. While FORWARD_HEADERS is on, the relay's own X-Forwarded-Host and X-Forwarded-Proto replace configured values unless trust_forwarded_headers is set, and a configured X-Forwarded-For becomes the start of the chain.
cors_originsstring[]noneOrigins allowed for CORS: exact origins, *.example.com, https://*.example.com, or *, compared case insensitively. When empty or absent the relay adds no CORS headers, and a disallowed Origin is answered without them rather than refused. See CORS and WAF.
cors_methodsstring[]GET, POST, PUT, PATCH, DELETE, OPTIONS, HEADAccess-Control-Allow-Methods.
cors_allowed_headersstring[]Content-Type, Authorization, X-MTE-Relay-RouteAccess-Control-Allow-Headers. A configured list replaces the default.
cors_exposed_headersstring[]Content-TypeAccess-Control-Expose-Headers. A configured list replaces the default, and the relay always appends its own response headers.
cors_allow_credentialsbooltrueSends Access-Control-Allow-Credentials: true.
cors_max_age_secondsint0Sends Access-Control-Max-Age when above 0.
cors_preflight_passthroughboolfalseStops the relay answering OPTIONS preflights with 204 itself. A preflight on a pass through route then goes to the upstream, and on an outbound domain to the next relay. A preflight to the encrypted route or an /api/mte-* endpoint then fails with 404 or 405, so leave it off on domains that serve browser SDKs.
preserve_hostboolfalseSends the client's host in the upstream Host header instead of the upstream's own authority. The dial address and TLS name still come from upstream. Not allowed together with outbound.
trust_forwarded_headersboolfalseKeeps an incoming X-Forwarded-Host and X-Forwarded-Proto instead of replacing them with what this relay observed. Enable it only on a domain fed by another relay.
forward_browser_cookiesbooltrueCopies the browser's Cookie header upstream when the frame carries none, and moves upstream Set-Cookie headers onto the relay's real HTTP response, where a browser can store them. Applies to encrypted inbound requests.
forward_plain_headersstring[]nonePlain headers from the client to relay hop that the relay copies upstream, matched by name without regard to case, with * wildcards such as x-trace-*. ["*"] forwards every plain header except a fixed exclude list. A header inside the encrypted frame wins, except traceparent: the relay always copies a plain hop traceparent upstream, whatever this list says. Applies to encrypted inbound requests.
request_secondsintREQUEST_SECONDSRequest silence deadline for this domain. 0 uses the global value.
response_secondsintRESPONSE_SECONDSWhole response bound for this domain's encrypted requests. Pass through routes and outbound domains ignore it. 0 uses the global value.
idle_secondsintIDLE_TIMEOUT_SECONDSResponse silence deadline for this domain's encrypted requests. On pass through routes and outbound domains it only limits each write to the client. 0 uses the global value.
ping_secondsintPING_INTERVAL_SECONDSPING cadence advertised for this domain. 0 uses the global value.
response_coalesce_msintRESPONSE_COALESCE_MSResponse coalescing window for this domain, from 0 to 1000. Unlike the timers, 0 turns coalescing off for the domain; leave the field out to use the global value.

Discovery advertises the per domain values a client needs: pass through routes, the plain header list, the cookie setting, and the per domain timers. See Discovery.

Headers the plain hop never forwards​

No forward_plain_headers entry, including *, can forward these: Host, Content-Type, Content-Length and every other Content-* header, Accept-Encoding, Cookie (governed by forward_browser_cookies), Expect, any X-MTE-Relay-* header, and hop by hop headers.

Forwarding headers​

The relay builds a new request for the upstream, so it stamps the standard forwarding headers unless FORWARD_HEADERS is false:

HeaderValue
X-Forwarded-HostThe host the client requested, port included. It replaces any value the client sent, in the clear or inside the frame, unless the domain sets trust_forwarded_headers.
X-Forwarded-ProtoA value from a trusted relay, then a load balancer's X-Forwarded-Proto, then https or http from the relay's own connection.
X-Forwarded-ForThe connection's peer address, appended to any existing chain. TRUSTED_PROXIES does not change it.

Examples​

A single API behind MTE Relay Server:

{
"api.example.com": {
"upstream": "http://api-service:8080",
"client_id_secret": "REPLACE_WITH_A_LONG_RANDOM_SECRET",
"cors_origins": ["https://app.example.com"],
"pass_through_routes": ["/health"]
}
}

Several services plus a catch all entry. Every entry here is an encrypted inbound domain; only the listed pass through routes accept plain HTTP.

{
"billing.example.com": {
"upstream": "http://billing-service:8080",
"client_id_secret": "REPLACE_WITH_SECRET_1",
"pass_through_routes": ["/ready", "/live"],
"cors_origins": ["https://app.example.com"]
},
"auth.example.com": {
"upstream": "http://auth-service:3000",
"client_id_secret": "REPLACE_WITH_SECRET_2",
"cors_origins": ["https://app.example.com"],
"cors_methods": ["GET", "POST", "OPTIONS"]
},
"*": {
"upstream": "http://default-backend:8080",
"client_id_secret": "REPLACE_WITH_SECRET_3"
}
}

A development setup where one port gets its own upstream:

{
"localhost:3000": { "upstream": "http://host.docker.internal:3000" },
"localhost": { "upstream": "http://host.docker.internal:4000" }
}

A tenant wildcard, with browser tracing headers forwarded from the plain hop. Because the list of allowed headers is customized, it keeps Content-Type and X-MTE-Relay-Route:

{
"*.tenants.example.com": {
"upstream": "http://tenant-api:8080",
"client_id_secret": "REPLACE_WITH_A_LONG_RANDOM_SECRET",
"cors_origins": ["https://*.tenants.example.com"],
"forward_plain_headers": ["traceparent", "tracestate"],
"cors_allowed_headers": ["Content-Type", "X-MTE-Relay-Route", "traceparent", "tracestate"]
}
}

To pass DOMAIN_MAP on one line, for example to docker run -e, keep the JSON in single quotes:

export DOMAIN_MAP='{"api.example.com":{"upstream":"http://api-service:8080","client_id_secret":"REPLACE_WITH_A_LONG_RANDOM_SECRET"}}'

Outbound domains​

An outbound domain turns the relay into the client side of a relay to relay chain, which is how MTE API Relay protects server to server traffic. Your service sends ordinary HTTP to the outbound relay. The relay frames each request, sends it to the next relay named in upstream, and returns the decoded response as ordinary HTTP with the origin's status.

  • The request's Host selects the outbound entry, so key the entry on the name your service uses to reach the outbound relay.
  • The path and query pass through unchanged. There are no special request headers; v4's x-mte-upstream and x-mte-outbound-token headers and the outbound_token field are gone.
  • The relay buffers the request body before framing it, under MAX_REQUEST_BODY_MIB.
  • Caller headers under the relay owned X-MTE-Relay- prefix are dropped, the domain's headers are applied, and the forwarding headers are stamped.
  • The first request pairs with the next relay. When the next relay rejects a request with a pair, token, or state store error before its origin ran it, the relay retries on a fresh pair, up to four times. Other errors from the next relay are not retried.
  • Errors on the plaintext side are JSON bodies with code, reason, message, and cause, plus the X-MTE-Relay-Error header. A failure at the next relay is 502, and cause names the next relay's code, reason, and hop. A host that matches no key, or does not parse, is answered before the relay knows the domain is outbound, so that 480 or 490 arrives as a binary ERROR frame; read the X-MTE-Relay-Error header. See Errors and recovery.
  • An outbound client idle for longer than SESSION_TIMEOUT_MINUTES is closed. A changed upstream or personalization_string rebuilds it.
  • The next relay must also run the MTE API Relay image, because the MAR MTE library build pairs only with another MAR.

A two relay chain looks like this. Relay A runs next to your service:

{
"relay-a": {
"upstream": "https://relay-b.partner.example.com",
"outbound": true
}
}

Your service then calls http://relay-a:8080/orders in place of the partner API. Relay B runs in front of the API:

{
"relay-b.partner.example.com": {
"upstream": "http://partner-api:8080",
"client_id_secret": "REPLACE_WITH_A_LONG_RANDOM_SECRET",
"trust_forwarded_headers": true
}
}

Relay A stamps its observed host into the encrypted frame, and trust_forwarded_headers on relay B keeps it, so the API sees the host your service asked for. preserve_host is refused on an outbound domain; set it on relay B's inbound domain when the API needs that host in its Host header.

Validation​

A map that breaks any of these rules is refused:

  • every key follows the key rules, and no two keys differ only by letter case;
  • every entry is an object with a non empty upstream;
  • upstream is an absolute http:// or https:// URL with a host;
  • outbound and preserve_host are not both set;
  • an outbound domain with an http:// upstream sets allow_plain_upstream;
  • request_seconds, response_seconds, idle_seconds, and ping_seconds are not negative;
  • response_coalesce_ms is from 0 to 1000;
  • forward_plain_headers entries are non empty HTTP header names with optional * wildcards, and * on its own is not combined with other entries.

A standalone relay also refuses an empty map. The control plane adds warnings that never block a save: a key it does not recognize (usually a misspelling, which workers ignore), a host without client_id_secret, and an upstream with a base path.

A relay that had to generate a client_id_secret logs one warning at startup that names every such host: Generated a client_id_secret for DOMAIN_MAP entries api.example.com: it does not survive a restart and is not shared across instances, so set client_id_secret explicitly for anything but a single local process. SocketX logs the same line.