From 499e070397350130b99658953fdd0a2def1db09a Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Tue, 20 Oct 2026 12:13:20 +0200 Subject: [PATCH] fix: support explicit PostgreSQL CA bundles without plaintext fallback --- src/db.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 12 ++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/db.rs b/src/db.rs index 2cd37a5..33454cc 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,7 +1,7 @@ use std::{path::Path, time::Duration}; -use native_tls::TlsConnector; -use postgres::{Client, Config}; +use native_tls::{Certificate, TlsConnector}; +use postgres::{Client, Config, config::SslMode}; use postgres_native_tls::MakeTlsConnector; use crate::Result; @@ -61,7 +61,11 @@ impl Database { if config.get_connect_timeout().is_none() { config.connect_timeout(Duration::from_secs(5)); } - let tls = MakeTlsConnector::new(TlsConnector::new()?); + let root_cert = std::env::var_os("PGSSLROOTCERT"); + let tls = MakeTlsConnector::new(postgres_tls( + config.get_ssl_mode(), + root_cert.as_deref().map(Path::new), + )?); let mut client = config.connect(tls)?; // Views can invoke functions: enforce read-only on the server, not just in the UI. client.batch_execute( @@ -276,6 +280,28 @@ impl Database { } } +fn postgres_tls(mode: SslMode, root_cert: Option<&Path>) -> Result { + let mut tls = TlsConnector::builder(); + if let Some(path) = root_cert { + if mode != SslMode::Require { + return Err("PGSSLROOTCERT requires Require TLS (sslmode=require); plaintext fallback is not allowed.".into()); + } + let pem = std::fs::read(path) + .map_err(|error| format!("Cannot read PGSSLROOTCERT CA file: {error}"))?; + let certificates = Certificate::stack_from_pem(&pem) + .map_err(|error| format!("Invalid PGSSLROOTCERT PEM CA bundle: {error}"))?; + if certificates.is_empty() { + return Err("PGSSLROOTCERT must contain at least one PEM CA certificate.".into()); + } + // An explicit bundle replaces system roots; certificate and hostname checks stay enabled. + tls.disable_built_in_roots(true); + for certificate in certificates { + tls.add_root_certificate(certificate); + } + } + Ok(tls.build()?) +} + fn page_offset(page: usize) -> Result { i64::try_from(page)? .checked_mul(PAGE_SIZE as i64) @@ -303,6 +329,23 @@ pub fn display_text(value: &str) -> String { mod tests { use super::*; + #[test] + fn explicit_ca_bundle_fails_closed_on_insecure_modes_and_invalid_files() -> Result<()> { + let bundle = tempfile::NamedTempFile::new()?; + for mode in [SslMode::Prefer, SslMode::Disable] { + let error = postgres_tls(mode, Some(bundle.path())).unwrap_err(); + assert!(error.to_string().contains("sslmode=require")); + } + for contents in ["", "not a PEM certificate", "-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----"] { + std::fs::write(bundle.path(), contents)?; + assert!(postgres_tls(SslMode::Require, Some(bundle.path())).is_err()); + } + let missing = bundle.path().with_extension("missing"); + let error = postgres_tls(SslMode::Require, Some(&missing)).unwrap_err(); + assert!(error.to_string().contains("Cannot read PGSSLROOTCERT")); + Ok(()) + } + #[test] fn sqlite_identity_is_filename_without_terminal_controls() -> Result<()> { let directory = tempfile::tempdir()?; diff --git a/src/main.rs b/src/main.rs index f6ce11f..1d47567 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,18 @@ Expansion never runs shell commands. Quote CLI paths to let dbdash expand them. PostgreSQL URLs and libpq-style key=value strings are accepted. Use sslmode=require for mandatory TLS; certificates and hostnames are verified. Use sslmode=disable only for trusted local connections. + +For private CAs (including Amazon RDS), set PGSSLROOTCERT to a PEM CA bundle: + PGSSLROOTCERT="$HOME/.config/dbdash/rds-us-east-1.pem" cargo run +This applies to saved and CLI PostgreSQL connections. Choose Require TLS in the +connection window, or sslmode=require on the CLI; Prefer/Disable are rejected. +Only the supplied bundle is trusted while set; macOS/system trust is unchanged. +Certificates and hostnames are verified before authentication; no plaintext fallback. +Missing, empty, or invalid CA files block the connection. Use a bundle obtained +from the trusted issuer, not the certificate presented by an unverified server. +For RDS us-east-1, download AWS's official regional bundle: + https://truststore.pki.rds.amazonaws.com/us-east-1/us-east-1-bundle.pem +Enter passwords in the in-app field; putting them in CLI arguments exposes them. The inspector needs 70×17 terminal cells; PostgreSQL/SQLite windows need 74×25/74×21. Navigation: