fix: switch PostgreSQL TLS from rustls to native-tls

rustls with rustls-native-certs fails TLS handshake on Railway's
slim container (empty or stale root cert store). native-tls delegates
to OpenSSL on Linux which handles system certs more reliably.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-03-25 19:49:55 -07:00
co-authored by Claude Opus 4.6
parent aa27c72cbb
commit ea2ef15489
2 changed files with 17 additions and 37 deletions
+4 -6
View File
@@ -54,9 +54,8 @@ deadpool-postgres = { version = "0.14", optional = true }
tokio-postgres = { version = "0.7", features = ["with-uuid-1", "with-chrono-0_4", "with-serde_json-1"], optional = true }
postgres-types = { version = "0.2", features = ["with-serde_json-1"], optional = true }
refinery = { version = "0.8", features = ["tokio-postgres"], optional = true }
tokio-postgres-rustls = { version = "0.13", optional = true }
rustls = { version = "0.23", optional = true, default-features = false }
rustls-native-certs = { version = "0.8", optional = true }
postgres-native-tls = { version = "0.5", optional = true }
native-tls = { version = "0.2", optional = true }
# Database - libSQL/Turso (optional embedded database)
libsql = { version = "0.6", optional = true, default-features = false, features = ["core", "replication", "remote", "tls"] }
@@ -216,9 +215,8 @@ default = ["postgres", "libsql", "html-to-markdown"]
postgres = [
"dep:deadpool-postgres",
"dep:tokio-postgres",
"dep:tokio-postgres-rustls",
"dep:rustls",
"dep:rustls-native-certs",
"dep:postgres-native-tls",
"dep:native-tls",
"dep:postgres-types",
"dep:refinery",
"dep:pgvector",
+13 -31
View File
@@ -1,13 +1,14 @@
//! TLS connector factory for PostgreSQL connections.
//!
//! Builds a [`deadpool_postgres::Pool`] with the appropriate TLS connector
//! based on the configured [`SslMode`]. Uses `rustls` with system root
//! certificates — the same TLS stack that `reqwest` already uses for HTTP.
//! based on the configured [`SslMode`]. Uses `native-tls` which delegates
//! to the platform's TLS library (OpenSSL on Linux, Secure Transport on macOS,
//! SChannel on Windows).
use deadpool_postgres::{Pool, Runtime};
use postgres_native_tls::MakeTlsConnector;
use thiserror::Error;
use tokio_postgres::NoTls;
use tokio_postgres_rustls::MakeRustlsConnect;
use crate::config::SslMode;
@@ -16,39 +17,21 @@ pub enum CreatePoolError {
#[error("{0}")]
Pool(#[from] deadpool_postgres::CreatePoolError),
#[error("postgres TLS configuration failed: {0}")]
TlsConfig(#[from] rustls::Error),
TlsConfig(#[from] native_tls::Error),
}
/// Build a rustls-based TLS connector using the platform's root certificate store.
fn make_rustls_connector() -> Result<MakeRustlsConnect, rustls::Error> {
let mut root_store = rustls::RootCertStore::empty();
let native = rustls_native_certs::load_native_certs();
for e in &native.errors {
tracing::warn!("error loading system root certs: {e}");
}
for cert in native.certs {
if let Err(e) = root_store.add(cert) {
tracing::warn!("skipping invalid system root cert: {e}");
}
}
if root_store.is_empty() {
tracing::error!("no system root certificates found -- TLS connections will fail");
}
// `--all-features` brings in both aws-lc-rs and ring-backed rustls providers.
// Pick the same ring provider reqwest already uses so postgres TLS setup stays deterministic.
let config = rustls::ClientConfig::builder_with_provider(
rustls::crypto::ring::default_provider().into(),
)
.with_safe_default_protocol_versions()?
.with_root_certificates(root_store)
.with_no_client_auth();
Ok(MakeRustlsConnect::new(config))
/// Build a native-tls connector using the platform's certificate store.
fn make_tls_connector() -> Result<MakeTlsConnector, native_tls::Error> {
let tls_connector = native_tls::TlsConnector::builder()
.danger_accept_invalid_certs(false)
.build()?;
Ok(MakeTlsConnector::new(tls_connector))
}
/// Create a [`deadpool_postgres::Pool`] with the appropriate TLS connector.
///
/// - `Disable` → plain TCP (no TLS)
/// - `Prefer` / `Require` → rustls with system root certificates
/// - `Prefer` / `Require` → native-tls with platform certificate store
///
/// **Note:** `Prefer` and `Require` currently behave identically — both
/// provide a TLS connector and will fail if the server rejects the TLS
@@ -65,7 +48,7 @@ pub fn create_pool(
.create_pool(Some(Runtime::Tokio1), NoTls)
.map_err(CreatePoolError::from),
SslMode::Prefer | SslMode::Require => {
let tls = make_rustls_connector()?;
let tls = make_tls_connector()?;
config
.create_pool(Some(Runtime::Tokio1), tls)
.map_err(CreatePoolError::from)
@@ -81,7 +64,6 @@ mod tests {
fn create_pool_disable_mode() {
let mut config = deadpool_postgres::Config::new();
config.url = Some("postgres://localhost/test".to_string());
// Should succeed — pool is created lazily, no actual connection needed.
let pool = create_pool(&config, SslMode::Disable);
assert!(pool.is_ok());
}