Skip to content

Routes & Domains

Routes are nested inside domain entries ([[domains]]). A domain groups a TLS certificate, per-domain headers and security policy, and one or more path-prefix routes. Dynamic: hot-reloadable.

Each [[domains]] entry handles one hostname (exact or wildcard). Omit host for a catch-all that matches any host, including plain-HTTP traffic, IP literals, and localhost.

domains:
- host: "api.example.com"
cert_path: "/config/certs/api.crt"
key_path: "/config/certs/api.key"
routes:
- prefix: "/api"
backend: "backend-a:9000"
- prefix: "/"
backend: "backend-b:9000"
domains:
- host: "*.example.com"
cert_path: "/config/certs/wildcard.crt"
key_path: "/config/certs/wildcard.key"
routes:
- prefix: "/"
backend: "backend-a:9000"

Wildcard matching is one level only: *.example.com matches api.example.com but not a.b.example.com.

Matches any host that has no explicit domain entry. Its certificate (if set) becomes the TLS default certificate (the fallback for connections that send no SNI or an unrecognized SNI). Omit cert/key for plain-HTTP only.

domains:
- routes:
- prefix: "/"
backend: "localhost:3000"

The request host is resolved from the HTTP-layer authority for all protocol versions: :authority (HTTP/2) or the Host header (HTTP/1.1). TLS SNI is not used for routing: it only selects the certificate.

Matching order:

  1. Exact: "api.example.com"
  2. Wildcard: "*.example.com" (one level only)
  3. Catch-all: the entry with no host key, if present
  4. No match → 421 Misdirected Request

Host values are lowercased at load and compared case-insensitively. The config is rejected if two domains share the same effective host or if more than one catch-all is defined.

Within a domain, routes are matched by URL prefix. The most specific (longest) prefix wins: sorted at config load time, so declaration order does not affect which route matches.

/api/v2 beats /api beats / for a request to /api/v2/users. Identical prefixes are treated as load-balance candidates (round-robin).

domains:
- host: "api.example.com"
routes:
- prefix: "/api/v2"
backend: "backend-a:9000"
- prefix: "/api"
backend: "backend-b:9000"
- prefix: "/"
backend: "backend-b:9000"

Per route you can:

  • Forward the path unchanged (omit replace_path)
  • Strip the matched prefix so the backend sees a shorter path (replace_path = "")
  • Rewrite the prefix to a different value (replace_path = "/new/...")

Query strings are preserved. There is no regular-expression routing, only prefix matching.

Strip: /strip/users → backend receives /users:

domains:
- host: "api.example.com"
routes:
- prefix: "/strip"
backend: "backend-a:9000"
replace_path: ""

Rewrite: /old/data → backend receives /new/data:

domains:
- host: "api.example.com"
routes:
- prefix: "/old"
backend: "backend-b:9000"
replace_path: "/new"

The fingerprinting field controls header injection for fingerprint data. Capture is always driven by the global [fingerprint] static config.

Resolution order: routedomain → global default (true).

domains:
- host: "api.example.com"
fingerprinting: true # domain-level default
routes:
- prefix: "/internal"
backend: "backend-a:9000"
fingerprinting: false # override for this route

Top-level boolean. When true, the original client Host header is forwarded to the upstream. When false (default), the request is forwarded with the backend address as its authority. Dynamic.

preserve_host: false

force_new_connection = true bypasses the connection pool and opens a fresh TCP+TLS connection per request to the upstream. Use it when you need a new TLS handshake for each request. Adds latency; avoid in high-throughput routes.

It does not affect client fingerprint capture: TLS JA4 and HTTP/2 signatures come from the client→proxy connection, and TCP SYN is captured via eBPF at the client→proxy edge.

domains:
- host: "api.example.com"
routes:
- prefix: "/isolated"
backend: "backend-a:9000"
force_new_connection: true
- prefix: "/api"
backend: "backend-a:9000"
force_new_connection: false

Pool settings are under [backend_pool].

  • prefix, backend, replace_path, fingerprinting, force_new_connection
  • Optional security: per-route ip_filter, rate_limit, headers (whole-block replaces). See Security and Rate limiting
  • Optional [domains.routes.headers]: same shape as global [headers]. See Headers
  • host (exact, *.wildcard, or absent for catch-all)
  • cert_path, key_path: TLS certificate for this domain (omit for plain HTTP)
  • fingerprinting: domain-level gate (resolved against route-level fingerprinting)
  • headers: domain-level header manipulation (same shape as global [headers])
  • security: per-domain overrides for ip_filter, rate_limit, headers (whole-block replaces)
  • routes: array of route entries (see above)

See Configuration overview for the full config layout.