fix: support explicit PostgreSQL CA bundles without plaintext fallback
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
use std::{path::Path, time::Duration};
|
use std::{path::Path, time::Duration};
|
||||||
|
|
||||||
use native_tls::TlsConnector;
|
use native_tls::{Certificate, TlsConnector};
|
||||||
use postgres::{Client, Config};
|
use postgres::{Client, Config, config::SslMode};
|
||||||
use postgres_native_tls::MakeTlsConnector;
|
use postgres_native_tls::MakeTlsConnector;
|
||||||
|
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
@@ -61,7 +61,11 @@ impl Database {
|
|||||||
if config.get_connect_timeout().is_none() {
|
if config.get_connect_timeout().is_none() {
|
||||||
config.connect_timeout(Duration::from_secs(5));
|
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)?;
|
let mut client = config.connect(tls)?;
|
||||||
// Views can invoke functions: enforce read-only on the server, not just in the UI.
|
// Views can invoke functions: enforce read-only on the server, not just in the UI.
|
||||||
client.batch_execute(
|
client.batch_execute(
|
||||||
@@ -276,6 +280,28 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn postgres_tls(mode: SslMode, root_cert: Option<&Path>) -> Result<TlsConnector> {
|
||||||
|
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> {
|
fn page_offset(page: usize) -> Result<i64> {
|
||||||
i64::try_from(page)?
|
i64::try_from(page)?
|
||||||
.checked_mul(PAGE_SIZE as i64)
|
.checked_mul(PAGE_SIZE as i64)
|
||||||
@@ -303,6 +329,23 @@ pub fn display_text(value: &str) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn sqlite_identity_is_filename_without_terminal_controls() -> Result<()> {
|
fn sqlite_identity_is_filename_without_terminal_controls() -> Result<()> {
|
||||||
let directory = tempfile::tempdir()?;
|
let directory = tempfile::tempdir()?;
|
||||||
|
|||||||
+12
@@ -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.
|
PostgreSQL URLs and libpq-style key=value strings are accepted.
|
||||||
Use sslmode=require for mandatory TLS; certificates and hostnames are verified.
|
Use sslmode=require for mandatory TLS; certificates and hostnames are verified.
|
||||||
Use sslmode=disable only for trusted local connections.
|
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.
|
The inspector needs 70×17 terminal cells; PostgreSQL/SQLite windows need 74×25/74×21.
|
||||||
|
|
||||||
Navigation:
|
Navigation:
|
||||||
|
|||||||
Reference in New Issue
Block a user