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.
How SNI and routing relate
Section titled “How SNI and routing relate”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.
Minimal HTTPS
Section titled “Minimal HTTPS”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"[tls]alpn = ["h2", "http/1.1"]
[[domains]]host = "api.example.com"cert_path = "/config/certs/api.crt"key_path = "/config/certs/api.key"
[[domains.routes]] prefix = "/" backend = "backend-a:9000"Multiple domains, multiple certificates
Section titled “Multiple domains, multiple certificates”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"[tls]alpn = ["h2", "http/1.1"]
[[domains]]host = "api.example.com"cert_path = "/config/certs/api.crt"key_path = "/config/certs/api.key"
[[domains.routes]] prefix = "/" backend = "api:9000"
[[domains]]host = "*.internal.example.com"cert_path = "/config/certs/wildcard-internal.crt"key_path = "/config/certs/wildcard-internal.key"
[[domains.routes]] prefix = "/" backend = "internal:9001"
[[domains]] # catch-all → default certcert_path = "/config/certs/default.crt"key_path = "/config/certs/default.key"
[[domains.routes]] prefix = "/" backend = "fallback:9002"Certificate rotation
Section titled “Certificate rotation”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.).
Options
Section titled “Options”Use [tls.options] to restrict cipher suites or enable SNI strict mode.
Note:
cipher_suitesandsni_strictare applied to the TLS stack.versions,min_version,max_version, andcurve_preferencesare 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[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 accessSNI strict mode
Section titled “SNI strict mode”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.
Misdirected requests (HTTP 421)
Section titled “Misdirected requests (HTTP 421)”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.
Client authentication (mTLS)
Section titled “Client authentication (mTLS)”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"[tls.client_auth]required = { ca_cert_path = "/config/certs/ca.crt" }Session resumption
Section titled “Session resumption”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[tls.session_resumption]enabled = truemax_sessions = 256Related
Section titled “Related”- Security: response headers (HSTS, CSP) and forwarding behavior
- Configuration overview: full top-level index