neondatabase/neon · error · QueryError
client did not connect with TLS
Error message
client did not connect with TLS
What it means
When the server has TLS configured (have_tls) and a plain StartupMessage arrives while the connection is not in the Encrypted state, the server rejects the session: it first writes an ErrorResponse 'must connect with TLS' to the client, then returns QueryError::Other with this internal message. This enforces that every client on a TLS-enabled listener upgrades before startup.
Source
Thrown at libs/postgres_backend/src/lib.rs:695
"direct SSL negotiation but no TLS support"
)));
}
if have_tls {
self.start_tls().await?;
self.state = ProtoState::Encrypted;
}
}
FeStartupPacket::GssEncRequest => {
debug!("GSS requested");
self.write_message(&BeMessage::EncryptionResponse(false))
.await?;
}
FeStartupPacket::StartupMessage { .. } => {
if have_tls && !matches!(self.state, ProtoState::Encrypted) {
self.write_message(&BeMessage::ErrorResponse("must connect with TLS", None))
.await?;
return Err(QueryError::Other(anyhow::anyhow!(
"client did not connect with TLS"
)));
}
// NB: startup() may change self.auth_type -- we are using that in proxy code
// to bypass auth for new users.
handler.startup(self, &msg)?;
match self.auth_type {
AuthType::Trust => {
self.write_message_noflush(&BeMessage::AuthenticationOk)?
.write_message_noflush(&BeMessage::CLIENT_ENCODING)?
.write_message_noflush(&BeMessage::INTEGER_DATETIMES)?
// The async python driver requires a valid server_version
.write_message_noflush(&BeMessage::server_version("14.1"))?
.write_message(&BeMessage::ReadyForQuery)
.await?;
self.state = ProtoState::Established;View on GitHub (pinned to 8f60b04da4)
Solutions
- Client-side: require TLS, e.g. sslmode=require in the connection string
- If the deployment intentionally allows plaintext (dev), start the server without TLS config instead of mixing
- Fix proxies/load balancers that terminate TLS and forward plaintext to a backend that still expects encryption
- Confirm the client actually upgrades after the SSLRequest 'Y' response (check for TLS handshake in logs/tcpdump)
Example fix
# before psql 'postgresql://user@host:5432/db?sslmode=disable' # server -> ERROR: must connect with TLS # after psql 'postgresql://user@host:5432/db?sslmode=require'
Defensive patterns
Strategy: validation
Validate before calling
// Client-side: build the connection with required TLS up front. let config = tokio_postgres::Config::from_str(&url)?; config.ssl_mode(tokio_postgres::config::SslMode::Require); // never Plain text // Server-side: advertise the requirement clearly in startup logs: // "TLS enabled: plaintext startups will be rejected with 'must connect with TLS'"
Try / catch
// Server side: the ErrorResponse 'must connect with TLS' is already sent to the
// client before this error is raised; just close and count the violation:
Err(QueryError::Other(e)) if e.to_string().contains("did not connect with TLS") => {
METRICS.plaintext_rejections.inc();
tracing::info!(peer = ?self.peer_addr, "rejected plaintext startup on TLS listener");
} Prevention
- Default every connection string to sslmode=require; treat sslmode=disable as a code smell needing justification
- Smoke-test each environment after enabling TLS with a plaintext probe -- expect the 'must connect with TLS' error
- Configure load balancers to pass through TLS or terminate AND re-encrypt, never terminate-to-plaintext on a TLS backend
- Automate certificate renewal so TLS mode is never turned off reactively
When it happens
Trigger: Connecting with sslmode=disable, or sslmode=prefer where the client declines the offered TLS (server answered 'N' to the SSLRequest, or client skips asking), to a PostgresBackend with tls_config set. Also hitting the endpoint through a plaintext proxy path while the backend requires encryption.
Common situations: Security hardening turning optional TLS into mandatory while old clients keep sslmode=prefer/disable; JDBC/ODBC defaults that avoid TLS; internal tooling pointing at the TLS-only port without TLS; localhost scripts assuming plaintext is always fine.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- direct SSL negotiation but no TLS support
- Unexpected message {:?} while waiting for handshake
- no private key found in {}
- Unexpected CancelRequest message during handshake
- unexpected message type: {msg:?}
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/ec6dd63d76f6cae5.
Report an issue: GitHub.