{"record":{"id":"43da8fcdb912d0e7","repo":"neondatabase/neon","slug":"direct-ssl-negotiation-but-no-tls-support","errorCode":null,"errorMessage":"direct SSL negotiation but no TLS support","messagePattern":"direct SSL negotiation but no TLS support","errorType":"exception","errorClass":"QueryError","httpStatus":null,"severity":"error","filePath":"libs/postgres_backend/src/lib.rs","lineNumber":676,"sourceCode":"    /// - transition to Authentication if auth type is NeonJWT.\n    /// - or perform TLS handshake -- then need to call this again to receive\n    ///   actual startup packet.\n    async fn process_startup_message(\n        &mut self,\n        handler: &mut impl Handler<IO>,\n        msg: FeStartupPacket,\n    ) -> Result<(), QueryError> {\n        assert!(self.state < ProtoState::Authentication);\n        let have_tls = self.tls_config.is_some();\n        match msg {\n            FeStartupPacket::SslRequest { direct } => {\n                debug!(\"SSL requested\");\n\n                if !direct {\n                    self.write_message(&BeMessage::EncryptionResponse(have_tls))\n                        .await?;\n                } else if !have_tls {\n                    return Err(QueryError::Other(anyhow::anyhow!(\n                        \"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?;","sourceCodeStart":658,"sourceCodeEnd":694,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/postgres_backend/src/lib.rs#L658-L694","documentation":"In process_startup_message, a FeStartupPacket::SslRequest with direct=true means the client initiated the modern direct TLS negotiation (TLS ClientHello as the very first bytes, no plaintext SSLRequest roundtrip). If the server was started without a TLS configuration (tls_config is None), it cannot honor direct SSL and raises QueryError::Other immediately.","triggerScenarios":"Connecting with a client/libpq version that uses direct SSL negotiation (e.g. sslmode=direct or newer postgres clients with direct-SSL support enabled by default) to a PostgresBackend started without --tls-cert/--tls-key (no tls_config). Non-direct SSLRequest on a TLS-less server is fine: the server just answers 'N' and continues in plaintext.","commonSituations":"Local dev deployments started without TLS flags while the client library was upgraded to one that prefers direct SSL; staging configs copied to production where TLS termination was expected elsewhere; proxies stripping TLS so the backend sees plaintext while the client assumes TLS.","solutions":["Start the server with TLS enabled: provide the certificate chain and private key so tls_config is Some","Client-side, disable direct SSL negotiation or fall back to the classic sslmode=prefer flow","If TLS terminates at a proxy in front, make sure the proxy handles direct-SSL detection (first-byte sniffing) itself","Verify with 'openssl s_client' whether the endpoint expects TLS from the first byte"],"exampleFix":"# before: server started without TLS, client uses direct SSL\npageserver ...   # no --tls-cert/--tls-key\npsql 'postgresql://...?sslmode=direct'\n# -> direct SSL negotiation but no TLS support\n\n# after: enable TLS on the server\npageserver ... --tls-cert=server.crt --tls-key=server.key","handlingStrategy":"validation","validationCode":"// Server-side: fail fast with a clear log if clients may use direct SSL but TLS is off.\nlet tls_config = match (cert_path, key_path) {\n    (Some(cert), Some(key)) => Some(\n        Arc::new(tls_certs::load_certified_key(&key, &cert).await?)\n    ),\n    _ => None,\n};\nif tls_config.is_none() {\n    tracing::warn!(\n        \"TLS not configured: clients using direct SSL negotiation or sslmode=require will be rejected\"\n    );\n}","typeGuard":null,"tryCatchPattern":"// Client-side: detect the failure and fall back to classic negotiation.\n// With tokio-postgres, prefer ssl_mode(Prefer) over custom direct-TLS sockets so a\n// TLS-less server keeps working; direct SSL only when you know the endpoint supports it.","preventionTips":["Enable TLS (--tls-cert/--tls-key) on any listener reachable by modern clients that default to direct SSL","In deployment docs, state explicitly whether each postgres endpoint is plaintext, optional-TLS, or TLS-only","Health-check TLS readiness (e.g. openssl s_client -noservername) right after deploy","Keep cert/key rotation automated so operators never disable TLS to 'fix' renewal"],"tags":["rust","postgres-protocol","tls","sslmode","configuration"],"backgroundTag":"tls-not-enabled-on-server","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}