We run a public Postgres-protocol service on Fly using the pg_tls handler, with a Fly-managed Let’s Encrypt cert for our custom domain. Clients using libpq 17 or newer (psql 17+, psycopg, pgAdmin 8.13+, etc.) can’t connect over TLS.
Setup
[[services]]
protocol = "tcp"
internal_port = 5432
[[services.ports]]
port = 5432
handlers = ["pg_tls"]
What happens
Since version 17, libpq always sends the ALPN protocol postgresql in the TLS ClientHello, and there’s no way to turn that off. The pg_tls handler seems to advertise only http/1.1. With no overlap, the proxy aborts the handshake:
$ psql "host=<our-domain> sslmode=require ..."
psql: error: connection to server at "<our-domain>" (...), port 5432 failed:
SSL error: tlsv1 alert no application protocol
With sslmode=prefer (the libpq default), the client silently falls back to an unencrypted connection, so credentials are sent in plaintext.
Reproduction with openssl
# Fails with alert 120 (no_application_protocol):
openssl s_client -connect <app>.fly.dev:5432 -starttls postgres -servername <app>.fly.dev -alpn postgresql
# Also fails:
openssl s_client -connect <app>.fly.dev:5432 -starttls postgres -servername <app>.fly.dev -alpn h2
# Succeeds, negotiating "ALPN protocol: http/1.1":
openssl s_client -connect <app>.fly.dev:5432 -starttls postgres -servername <app>.fly.dev -alpn http/1.1
What we tried
A workaround in an earlier community thread (Migrate from Unmanaged Postgres to Neon) is to set:
tls_options = { alpn = ["postgresql"] }
Both fly deploy and a direct Machines API update now reject this:
Services [0] Ports [0] Tls options Alpn must be one of: h2, http/1.1
Setting alpn: [] through the Machines API is accepted but stored as tls_options: {}, and the behavior doesn’t change. We’re on flyctl v0.4.120.
Request
Could the pg_tls handler either:
- advertise/accept the
postgresqlALPN by default (the same check libpq 17+ makes against Postgres itself), or - allow
postgresqlas a value intls_options.alpn?
As things stand, pg_tls can’t serve TLS to any current libpq-based client, which is most Postgres tooling. Fly-managed certs can’t be exported, so terminating TLS in our own app would mean renewing certs ourselves.