Skip to content

TLS

TLS configuration splits into two places:

  • Certificate paths (cert_path, key_path) and optional mTLS (client_ca_path) belong to each [[domains]] entry. Each hostname can have its own certificate and client-CA bundle. See Routes & Domains.
  • Transport options (alpn, [tls.options], [tls.session_resumption]) live under the top-level [tls] section. Static: requires restart. Certificate file contents reload separately (see Certificate rotation).

Omit the [tls] section entirely to run as plain HTTP.

These are independent:

Layer Input What it selects
TLS handshake ClientHello SNI Which certificate (and mTLS verifier) to present
HTTP request :authority / Host Which domain + route (and backend)

SNI is not used for routing. That keeps HTTP/2 connection coalescing correct: one TLS session can carry requests for several hosts that share a certificate, each with its own :authority.

Certificate selection (SNI): exact host → single-label wildcard (*.example.com) → default cert (the catch-all / host-less domain). No SNI (e.g. IP-literal clients) also gets the default cert when sni_strict is off.

Set cert_path and key_path on each domain, and declare ALPN under [tls]:

tls:
alpn:
- "h2"
- "http/1.1"
domains:
- host: "api.example.com"
cert_path: "/config/certs/api.crt"
key_path: "/config/certs/api.key"
routes:
- prefix: "/"
backend: "backend-a:9000"

Each [[domains]] entry can carry its own certificate. The catch-all domain (no host) provides the default certificate, used when SNI is absent or unrecognized (unless sni_strict is on).

tls:
alpn: ["h2", "http/1.1"]
domains:
- host: "api.example.com"
cert_path: "/config/certs/api.crt"
key_path: "/config/certs/api.key"
routes:
- prefix: "/"
backend: "api:9000"
- host: "*.internal.example.com"
cert_path: "/config/certs/wildcard-internal.crt"
key_path: "/config/certs/wildcard-internal.key"
routes:
- prefix: "/"
backend: "internal:9001"
- # catch-all → default cert
cert_path: "/config/certs/default.crt"
key_path: "/config/certs/default.key"
routes:
- prefix: "/"
backend: "fallback:9002"

Certificates are not watched on their own in isolation from config. The proxy re-reads cert/key (and client_ca_path) on every config reload: SIGHUP, or a change to the config file / PEM paths when [reload].watch is enabled (default true, debounced by [reload].debounce_secs, default 60). Paths stay the same; new connections pick up the new certificate, existing ones keep the old until they close.

Reload is best-effort per domain: if one domain’s new cert fails to load, that domain keeps its previously serving certificate while the others still swap. Cipher suites, ALPN, and session-resumption on/off stay as built at startup ([tls] is static); the process-wide ticket key is shared across rebuilds so tickets remain valid after a cert reload.

There is no built-in ACME; point config at files another process renews (cert-manager, acme.sh, Vault, etc.).

Use [tls.options] to restrict versions, cipher suites, key-exchange groups, or enable SNI strict mode. These settings are enforced by the TLS stack.

TLS versions: versions (or min_version / max_version) restrict what the acceptor offers. Empty / unset keeps TLS 1.2 and 1.3. Do not set both an explicit versions list and a min/max bound.

Order matters for cipher_suites and curve_preferences: first entry is most preferred. A non-empty list replaces the provider defaults entirely (it does not merge).

Curves: leaving curve_preferences empty keeps the provider’s post-quantum-first defaults. Supported groups: X25519MLKEM768, SECP256R1MLKEM768 (post-quantum hybrids), X25519, secp256r1, secp384r1. If you set a list, keep a PQ hybrid first if you want PQ protection.

tls:
alpn:
- "h2"
- "http/1.1"
options:
sni_strict: false
versions:
- "1.2"
- "1.3"
cipher_suites:
- "TLS13_AES_128_GCM_SHA256"
- "TLS13_AES_256_GCM_SHA384"
- "TLS13_CHACHA20_POLY1305_SHA256"
- "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
- "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
- "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"
- "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
- "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
- "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
curve_preferences:
- "X25519MLKEM768"
- "SECP256R1MLKEM768"
- "X25519"
- "secp256r1"
- "secp384r1"

sni_strict = true disables the default-certificate fallback entirely (parity with Traefik’s sniStrict: true):

  • A connection whose SNI matches no domain cert is rejected with unrecognized_name.
  • A connection that sends no SNI (IP-literal clients, RFC 6066) is also rejected.

Leave it false (default) if you need IP-literal HTTPS access or no-SNI clients to work via the catch-all / default cert.

On coalesced HTTP/2 connections, any request whose host is served by a different certificate than the one the SNI selected is rejected automatically with 421 Misdirected Request. This is the same default behaviour as nginx and Apache mod_http2 and is not configurable. Hosts sharing one certificate (wildcard or SAN) still coalesce normally.

mTLS is per-domain via client_ca_path on a [[domains]] entry — there is no listener-wide [tls.client_auth]. Domains with a CA bundle require a client certificate; domains without it stay public. One listener can mix both. mTLS domains never resume a TLS session (client cert re-verified every connection).

domains:
- host: "admin.example.com"
cert_path: "/config/certs/admin.crt"
key_path: "/config/certs/admin.key"
client_ca_path: "/config/certs/ca.crt"
routes:
- prefix: "/"
backend: "admin:9000"

A request whose Host names an mTLS domain is rejected with 421 unless it arrives over a TLS session established for that same domain (plaintext / no-SNI / foreign SNI are rejected). Failures to load cert or CA material never fall back to a config without a client verifier. Details: SETTINGS.md — Mutual TLS.

Stateless session tickets only (TLS 1.2 and 1.3). There is no server-side session cache and no max_sessions key. Enabled by default; toggle with session_resumption.enabled. mTLS domains never resume.

tls:
session_resumption:
enabled: true