{"record":{"id":"ec6dd63d76f6cae5","repo":"neondatabase/neon","slug":"client-did-not-connect-with-tls","errorCode":null,"errorMessage":"client did not connect with TLS","messagePattern":"client did not connect with TLS","errorType":"exception","errorClass":"QueryError","httpStatus":null,"severity":"error","filePath":"libs/postgres_backend/src/lib.rs","lineNumber":695,"sourceCode":"                        \"direct SSL negotiation but no TLS support\"\n                    )));\n                }\n\n                if have_tls {\n                    self.start_tls().await?;\n                    self.state = ProtoState::Encrypted;\n                }\n            }\n            FeStartupPacket::GssEncRequest => {\n                debug!(\"GSS requested\");\n                self.write_message(&BeMessage::EncryptionResponse(false))\n                    .await?;\n            }\n            FeStartupPacket::StartupMessage { .. } => {\n                if have_tls && !matches!(self.state, ProtoState::Encrypted) {\n                    self.write_message(&BeMessage::ErrorResponse(\"must connect with TLS\", None))\n                        .await?;\n                    return Err(QueryError::Other(anyhow::anyhow!(\n                        \"client did not connect with TLS\"\n                    )));\n                }\n\n                // NB: startup() may change self.auth_type -- we are using that in proxy code\n                // to bypass auth for new users.\n                handler.startup(self, &msg)?;\n\n                match self.auth_type {\n                    AuthType::Trust => {\n                        self.write_message_noflush(&BeMessage::AuthenticationOk)?\n                            .write_message_noflush(&BeMessage::CLIENT_ENCODING)?\n                            .write_message_noflush(&BeMessage::INTEGER_DATETIMES)?\n                            // The async python driver requires a valid server_version\n                            .write_message_noflush(&BeMessage::server_version(\"14.1\"))?\n                            .write_message(&BeMessage::ReadyForQuery)\n                            .await?;\n                        self.state = ProtoState::Established;","sourceCodeStart":677,"sourceCodeEnd":713,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/postgres_backend/src/lib.rs#L677-L713","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"# before\npsql 'postgresql://user@host:5432/db?sslmode=disable'\n# server -> ERROR: must connect with TLS\n\n# after\npsql 'postgresql://user@host:5432/db?sslmode=require'","handlingStrategy":"validation","validationCode":"// Client-side: build the connection with required TLS up front.\nlet config = tokio_postgres::Config::from_str(&url)?;\nconfig.ssl_mode(tokio_postgres::config::SslMode::Require); // never Plain text\n\n// Server-side: advertise the requirement clearly in startup logs:\n// \"TLS enabled: plaintext startups will be rejected with 'must connect with TLS'\"","typeGuard":null,"tryCatchPattern":"// Server side: the ErrorResponse 'must connect with TLS' is already sent to the\n// client before this error is raised; just close and count the violation:\nErr(QueryError::Other(e)) if e.to_string().contains(\"did not connect with TLS\") => {\n    METRICS.plaintext_rejections.inc();\n    tracing::info!(peer = ?self.peer_addr, \"rejected plaintext startup on TLS listener\");\n}","preventionTips":["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"],"tags":["rust","postgres-protocol","tls","sslmode","authentication"],"backgroundTag":"tls-required-by-server","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}