Skip to content

TLS

TLS configuration splits into two places:

  • Certificate paths (cert_path, key_path) belong to each [[domains]] entry. Each hostname can have its own certificate. See Routes & Domains.
  • Transport options (alpn, [tls.options], [tls.client_auth], [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 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. Replacing only the PEM files (e.g. a Kubernetes Secret) does not reload them. The proxy re-reads cert/key paths on every config reload: either SIGHUP, or a change to the config file when HUGINN_WATCH=true (debounced by HUGINN_WATCH_DELAY_SECS, default 60s). 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, mTLS, and session resumption stay as built at startup ([tls] is static).

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

Use [tls.options] to restrict cipher suites or enable SNI strict mode.

Note: cipher_suites and sni_strict are applied to the TLS stack. versions, min_version, max_version, and curve_preferences are currently parsed and validated at load time but not yet enforced. The TLS stack runs with rustls’ safe defaults (TLS 1.2 and 1.3, default curves). Do not rely on those four keys to restrict the negotiated version or curves yet.

tls:
options:
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_RSA_WITH_AES_128_GCM_SHA256"
curve_preferences:
- "X25519"
- "secp256r1"
- "secp384r1"
sni_strict: false # set true in production to reject unknown-hostname / no-SNI access

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.

Omit [tls.client_auth] to disable. Static. Global only: either all routes require client certs, or none do.

tls:
client_auth:
required:
ca_cert_path: "/config/certs/ca.crt"

Enabled by default. TLS 1.2 uses a server-side session cache; TLS 1.3 uses session tickets.

tls:
session_resumption:
enabled: true
max_sessions: 256