Reported: fallback SSL doesn't work in the Docker build. Reproduced —
it's worse than the fallback specifically: TLS was broken for nearly
every connection, including ones with no SNI at all:
$ curl -vk https://127.0.0.1/
* TLSv1.3 (IN), TLS alert, internal error (592)
* OpenSSL/3.0.13: error:0A000438:SSL routines::tlsv1 alert internal error
Root cause: targetinfo.lua's M.get() is shared by two call sites in
two incompatible nginx phases —
- proxy.conf's access_by_lua_block (a normal HTTP request phase,
where ngx.exit() is valid)
- nginx.conf's request_domain callback, which runs during the TLS
handshake itself (ssl_certificate_by_lua*), where ngx.exit() is
NOT a supported API
M.get() called ngx.exit() on every lookup failure (no domain/SNI, a
Redis error, or an unregistered host). When invoked from the SSL
phase, that aborted the handshake with a bare "internal error" alert
and produced no log output anywhere — silent and total, not limited
to the unregistered-domain case, since even a connection with no SNI
hits the same code path immediately.
Fix: M.get() no longer calls ngx.exit() itself — it returns
(nil, httpStatus) on failure. proxy.conf now checks the return value
and calls ngx.exit() itself (the phase where that's actually
supported). nginx.conf's request_domain guards the now-possibly-nil
result before indexing it, and leaves ngx.ctx.toAllow unset on
failure so allow_domain() correctly denies issuance and auto-ssl
falls through to the static fallback cert in autossl.conf.
Verified end to end against a running Docker build (deployed the
changed files into a live container and reloaded, rather than relying
on a full rebuild each iteration):
- No SNI at all: TLS now completes; HTTP layer correctly returns 406
(previously: broken handshake, no response at all)
- Unregistered SNI: same — TLS completes, 406, and openssl s_client
confirms the cert served is genuinely the fallback
(CN=sni-support-required-for-valid-ssl)
- A real registered Host: TLS completes and proxies through to the
backend correctly (confirms the success path is unaffected)
- npm test: 192/192 pass
Hosts that share one wildcard cert (e.g. hassio.718it.biz and
metrics.718it.biz under *.718it.biz) resolve to the same IP, so browsers
coalesce them onto a single HTTP/2 connection. The SSL request_domain
phase resolves the connection's first host and caches it in
ngx.ctx.targetInfo; the unguarded `if targetInfo then return targetInfo`
then handed that first host's target to every coalesced request on the
connection -- e.g. hassio.718it.biz served from metrics' 192.168.1.8:8088.
Confirmed via debug logging: for GET requests to hassio.718it.biz on a
coalesced connection, ngx.ctx already held metrics' IP and the function
short-circuited to it.
Guard the reuse by host: only return the cached target when
ngx.ctx.targetInfo_domain matches the requested domain, and record that
domain whenever a target is resolved. A coalesced request for a different
host now re-resolves against its actual Host header.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- host.remove() now deletes the cert under the host key instead of the
Domain relation object, so certs are actually removed from redis
- targetinfo.lua returns the redis connection to the pool via
set_keepalive instead of leaking one connection per request
- autossl.conf drops TLSv1/1.1 and 3DES, adds TLSv1.3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>