l4postgres: add Postgres STARTTLS (terminate inbound + re-originate outbound)#432
l4postgres: add Postgres STARTTLS (terminate inbound + re-originate outbound)#432tannevaled wants to merge 4 commits into
Conversation
PostgreSQL negotiates TLS with an 8-byte SSLRequest answered by 'S'/'N' before the handshake, so the generic `tls` handler (which expects a ClientHello first) cannot terminate a Postgres connection on its own. Add a small handler that reads the SSLRequest, replies 'S', and hands off to the next handler — so a following `tls` handler terminates TLS and downstream matchers can match the now-cleartext startup message (user, database, application_name). No TLS code is duplicated: it composes with the existing `tls` handler. Proof-of-concept to complement mholt#188 / the SSL discussion in mholt#187. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Outbound counterpart to the inbound termination: re-originate TLS to a
Postgres upstream that expects sslmode != disable. It performs the same
SSLRequest -> 'S' negotiation as a client, then a tls.Client handshake,
returning the encrypted connection ('N' or any other reply is an error).
Kept self-contained (a reusable function) rather than wired into l4proxy,
to avoid coupling the generic proxy to Postgres; intended to be plugged in
behind an opt-in upstream STARTTLS hook (left for discussion). Terminate-
only deployments (backend reached over a trusted/encrypted network) don't
need this.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thank you for your latest contributions! Please add integration tests and documentation. The Postgres-over-TLS example may also require updating. The config example in the why section above seems a bit strange - could you please explain why you need two |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a caddyfile_adapt integration test for postgres_starttls (a corrected, working example that does not depend on unmerged matchers) and a handler doc page. Addresses review feedback on mholt#432. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
You're right, and thanks for the careful read — the example in the description was wrong as written: it mixed the I've fixed it and added what you asked for:
On The PR description is updated accordingly. Thanks again — and please tell me if you'd rather I split the outbound |
What
Adds PostgreSQL STARTTLS support to
l4postgres, in both directions:postgres_starttlshandler: reads the 8-byteSSLRequest, repliesS, and hands off. Put atlshandler next to terminate TLS; the handlers/matchers that follow then see the cleartext startup message (user,database,application_name). It reuses the existingtlshandler (no TLS code duplicated).StartTLSClient(conn, *tls.Config): re-originates TLS to a Postgres upstream that expectssslmode != disable(sendsSSLRequest, expectsS, performs atls.Clienthandshake).Why
PostgreSQL doesn't begin TLS with a ClientHello — a libpq client sends
SSLRequestand waits forS/Nbefore the handshake (see #187). So the generictlshandler can't terminate a classic Postgres connection on its own. (PostgreSQL 17+ can negotiate TLS directly via ALPNpostgresql, which the existingtlsmatcher/handler already covers — seedocs/examples/postgres-over-tls.md; this handler is for the classicSSLRequestflow.)Working example (verified by a
caddyfile_adaptintegration test in this PR):{ layer4 { :5432 { @pg postgres route @pg { postgres_starttls tls proxy upstream.local:5432 } } } }Once cleartext, this composes with content matchers such as those proposed in #188 — e.g. adding a
postgres { database acme }matcher (from that PR) inside the route aftertlsto route by database. (#188 isn't merged, so it's intentionally left out of the example above.)Notes / design
l4proxy, to avoid coupling the generic proxy to Postgres. It would slot in behind an opt-in upstream STARTTLS hook — happy to shape that in review, or split the outbound piece into its own PR. Terminate-only deployments (backend on a trusted/encrypted network) don't need the outbound side.Tests / docs
starttls_test.go: inbound (repliesS/ rejects non-SSLRequest / read & write error branches), outbound (real TLS handshake against a self-signed test server /Nrefusal / write, reply-read, unexpected-reply, and handshake error branches), and Caddyfile parsing. 100% statement coverage of the new code.integration/caddyfile_adapt/gd_handler_postgres_starttls.caddytest: verifies the example above adapts to the expected JSON.docs/handlers/postgres_starttls.md.go test ./... -race,gofmt,go vet,golangci-lintclean; no go.mod change.Builds on #187 and complements #188.