use std::net::Ipv4Addr;
use sqlx_postgres::{PgConnectOptions, PgSslMode};
pub(crate) fn require_tls_for_remote(opts: &PgConnectOptions) -> Result<(), String> {
if opts.get_socket().is_some() {
return Ok(());
}
let host = opts.get_host();
if is_loopback(host) {
return Ok(());
}
match opts.get_ssl_mode() {
PgSslMode::VerifyCa | PgSslMode::VerifyFull => Ok(()),
insecure => Err(format!(
"refusing a Postgres connection to {host:?} with sslmode={insecure:?}: it does not \
authenticate the server (disable/allow/prefer can downgrade to plaintext; require \
completes TLS against any certificate, so an active man-in-the-middle can present a \
self-signed cert and read the database credential). Add ?sslmode=verify-full \
(recommended) or at least ?sslmode=verify-ca to the URL."
)),
}
}
fn is_loopback(host: &str) -> bool {
let normalized = host.trim_end_matches('.').to_ascii_lowercase();
if normalized == "localhost" || normalized == "[::1]" {
return true;
}
host.parse::<Ipv4Addr>()
.map(|v4| v4.is_loopback())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn opts(url: &str) -> PgConnectOptions {
PgConnectOptions::from_str(url).expect("valid test URL")
}
#[test]
fn a_remote_tcp_url_without_sslmode_is_refused() {
let err = require_tls_for_remote(&opts("postgres://u:p@db.example.com:5432/audit"))
.expect_err("Prefer default must be refused for a remote host");
assert!(
err.contains("sslmode"),
"message should name sslmode: {err}"
);
assert!(
err.contains("db.example.com"),
"message should name the host: {err}"
);
}
#[test]
fn a_remote_tcp_url_with_sslmode_disable_is_refused() {
require_tls_for_remote(&opts("postgres://u:p@db.example.com/audit?sslmode=disable"))
.expect_err("disable must be refused");
}
#[test]
fn a_remote_tcp_url_with_sslmode_prefer_is_refused() {
require_tls_for_remote(&opts("postgres://u:p@db.example.com/audit?sslmode=prefer"))
.expect_err("prefer (downgradeable) must be refused");
}
#[test]
fn a_remote_tcp_url_with_sslmode_allow_is_refused() {
require_tls_for_remote(&opts("postgres://u:p@db.example.com/audit?sslmode=allow"))
.expect_err("allow (plaintext-first) must be refused");
}
#[test]
fn a_remote_tcp_url_with_sslmode_require_is_refused() {
require_tls_for_remote(&opts("postgres://u:p@db.example.com/audit?sslmode=require"))
.expect_err("require does not authenticate the server and must be refused");
}
#[test]
fn a_remote_tcp_url_with_verify_ca_is_allowed() {
require_tls_for_remote(&opts(
"postgres://u:p@db.example.com/audit?sslmode=verify-ca",
))
.expect("verify-ca authenticates the cert chain and is the floor");
}
#[test]
fn a_remote_tcp_url_with_verify_full_is_allowed() {
require_tls_for_remote(&opts(
"postgres://u:p@db.example.com/audit?sslmode=verify-full",
))
.expect("verify-full must be allowed");
}
#[test]
fn a_remote_host_built_without_a_url_is_refused() {
let o = PgConnectOptions::new()
.host("db.example.com")
.username("u")
.database("audit")
.ssl_mode(PgSslMode::Disable);
require_tls_for_remote(&o).expect_err("a builder-constructed remote host must be refused");
}
#[test]
fn a_loopback_host_without_sslmode_is_allowed() {
require_tls_for_remote(&opts("postgres://u:p@localhost:5432/audit"))
.expect("localhost never crosses the network");
require_tls_for_remote(&opts("postgres://u:p@127.0.0.1:5432/audit"))
.expect("127.0.0.1 is loopback");
}
#[test]
fn an_ipv6_loopback_url_is_allowed() {
require_tls_for_remote(&opts("postgres://u:p@[::1]:5432/audit"))
.expect("[::1] is IPv6 loopback and never crosses the network");
}
#[test]
fn a_unix_socket_without_sslmode_is_allowed() {
let o = PgConnectOptions::new()
.socket("/var/run/postgresql")
.username("u")
.database("audit");
assert!(o.get_socket().is_some(), "should be a unix socket");
require_tls_for_remote(&o).expect("unix socket never crosses the network");
}
#[test]
fn a_non_loopback_ip_without_sslmode_is_refused() {
require_tls_for_remote(&opts("postgres://u:p@10.0.0.5:5432/audit"))
.expect_err("a private-range remote IP is still on the network");
}
}