diff --git a/.schema/users.schema.json b/.schema/users.schema.json index ebe188a33..4f64800ee 100644 --- a/.schema/users.schema.json +++ b/.schema/users.schema.json @@ -117,6 +117,13 @@ "type": "string" } }, + "identity": { + "description": "User identity used for mTLS.", + "type": [ + "string", + "null" + ] + }, "idle_timeout": { "description": "Overrides [`idle_timeout`](https://docs.pgdog.dev/configuration/pgdog.toml/general/#idle_timeout) for this user. Server connections that have been idle for this long, without affecting [`min_pool_size`](https://docs.pgdog.dev/configuration/pgdog.toml/general/#min_pool_size), will be closed.\n\nhttps://docs.pgdog.dev/configuration/users.toml/users/#idle_timeout", "type": [ diff --git a/integration/tls/dev.sh b/integration/tls/dev.sh index db2d7f5cc..53eb2dc99 100755 --- a/integration/tls/dev.sh +++ b/integration/tls/dev.sh @@ -5,6 +5,8 @@ DIR="$(cd "$(dirname "$0")" && pwd)" HOST=127.0.0.1 PORT=6432 DB=pgdog +USER_A=tls_user_a +USER_B=tls_user_b run_psql() { psql "host=$HOST port=$PORT dbname=$DB user=$1 sslmode=require sslcert=$DIR/$2.crt sslkey=$DIR/$2.key" -c "SELECT 1" > /dev/null 2>&1 @@ -19,9 +21,9 @@ FAIL=0 echo "=== TLS client certificate tests ===" -# Test 1: pgdog user with pgdog cert (should succeed) -echo -n "pgdog user + pgdog cert: " -if run_psql pgdog client; then +# Test 1: user identity pgdog with pgdog cert (should succeed) +echo -n "$USER_A identity pgdog + pgdog cert: " +if run_psql "$USER_A" client; then echo "OK" PASS=$((PASS + 1)) else @@ -29,9 +31,9 @@ else FAIL=$((FAIL + 1)) fi -# Test 2: pgdog2 user with pgdog2 cert (should succeed) -echo -n "pgdog2 user + pgdog2 cert: " -if run_psql pgdog2 client2; then +# Test 2: user identity pgdog2 with pgdog2 cert (should succeed) +echo -n "$USER_B identity pgdog2 + pgdog2 cert: " +if run_psql "$USER_B" client2; then echo "OK" PASS=$((PASS + 1)) else @@ -39,9 +41,9 @@ else FAIL=$((FAIL + 1)) fi -# Test 3: pgdog user with pgdog2 cert (should fail) -echo -n "pgdog user + pgdog2 cert: " -if run_psql pgdog client2; then +# Test 3: user identity pgdog with pgdog2 cert (should fail) +echo -n "$USER_A identity pgdog + pgdog2 cert: " +if run_psql "$USER_A" client2; then echo "FAIL (expected rejection)" FAIL=$((FAIL + 1)) else @@ -49,9 +51,9 @@ else PASS=$((PASS + 1)) fi -# Test 4: pgdog2 user with pgdog cert (should fail) -echo -n "pgdog2 user + pgdog cert: " -if run_psql pgdog2 client; then +# Test 4: user identity pgdog2 with pgdog cert (should fail) +echo -n "$USER_B identity pgdog2 + pgdog cert: " +if run_psql "$USER_B" client; then echo "FAIL (expected rejection)" FAIL=$((FAIL + 1)) else @@ -61,7 +63,7 @@ fi # Test 5: no TLS at all (should fail) echo -n "no TLS: " -if psql "host=$HOST port=$PORT dbname=$DB user=pgdog sslmode=disable" -c "SELECT 1" > /dev/null 2>&1; then +if psql "host=$HOST port=$PORT dbname=$DB user=$USER_A sslmode=disable" -c "SELECT 1" > /dev/null 2>&1; then echo "FAIL (expected rejection)" FAIL=$((FAIL + 1)) else diff --git a/integration/tls/run.sh b/integration/tls/run.sh index 80d0c497e..c108180e1 100755 --- a/integration/tls/run.sh +++ b/integration/tls/run.sh @@ -17,7 +17,7 @@ PID="" if [ -f "${PID_FILE}" ]; then PID=$(cat "${PID_FILE}") fi -while ! run_psql pgdog client; do +while ! run_psql tls_user_a client; do if [ -n "${PID}" ] && ! kill -0 "${PID}" 2> /dev/null; then echo "PgDog process (pid ${PID}) exited before becoming ready" exit 1 diff --git a/integration/tls/users.toml b/integration/tls/users.toml index 4721c4d5c..e4ce9d048 100644 --- a/integration/tls/users.toml +++ b/integration/tls/users.toml @@ -1,10 +1,13 @@ [[users]] -name = "pgdog" +name = "tls_user_a" database = "pgdog" +identity = "pgdog" +server_user = "pgdog" server_password = "pgdog" [[users]] -name = "pgdog2" +name = "tls_user_b" database = "pgdog" +identity = "pgdog2" server_user = "pgdog" server_password = "pgdog" diff --git a/pgdog-config/src/users.rs b/pgdog-config/src/users.rs index 9628d6509..7c8a87b27 100644 --- a/pgdog-config/src/users.rs +++ b/pgdog-config/src/users.rs @@ -49,9 +49,9 @@ impl Users { pub fn check(&mut self, config: &Config) { for user in &mut self.users { if user.passwords().is_empty() { - if !config.general.passthrough_auth() { + if !config.general.passthrough_auth() && user.identity.is_none() { warn!( - r#"user "{}" (database "{}") doesn't have a password and passthrough auth is disabled"#, + r#"user "{}" (database "{}") doesn't have a password, passthrough auth and mTLS are disabled"#, user.name, user.database, ); } @@ -64,7 +64,10 @@ impl Users { }; for database in databases { - if min_pool_size > 0 { + if min_pool_size > 0 + && user.server_password.is_none() + && user.server_auth == ServerAuth::Password + { warn!( r#"user "{}" (database "{}") does not have a password configured, PgDog cannot connect to the server to maintain "min_pool_size" of {}, setting it to 0"#, user.name, database, min_pool_size @@ -192,6 +195,8 @@ impl Display for PasswordKind { )] #[serde(deny_unknown_fields)] pub struct User { + /// User identity used for mTLS. + pub identity: Option, /// Name of the user. Clients that connect to PgDog will need to use this username. /// /// https://docs.pgdog.dev/configuration/users.toml/users/#name diff --git a/pgdog/src/backend/databases.rs b/pgdog/src/backend/databases.rs index 9c7a7da87..0fb47b498 100644 --- a/pgdog/src/backend/databases.rs +++ b/pgdog/src/backend/databases.rs @@ -314,6 +314,13 @@ impl Databases { } } + /// Get the user TLS identity. + pub fn identity(&self, user: impl ToUser) -> Option<&str> { + self.databases + .get(&user.to_user()) + .and_then(|cluster| cluster.identity()) + } + /// Get a cluster for the user/database pair if it's configured. pub fn cluster(&self, user: impl ToUser) -> Result { let user = user.to_user(); diff --git a/pgdog/src/backend/pool/cluster.rs b/pgdog/src/backend/pool/cluster.rs index 9e5231169..5a35c0951 100644 --- a/pgdog/src/backend/pool/cluster.rs +++ b/pgdog/src/backend/pool/cluster.rs @@ -87,6 +87,7 @@ pub struct Cluster { resharding_replication_retry_min_delay: Duration, regex_parser: RegexParser, mutual_tls: bool, + identity: Option, } /// Sharding configuration from the cluster. @@ -174,6 +175,7 @@ pub struct ClusterConfig<'a> { pub regex_parser_limit: usize, pub pub_sub_enabled: bool, pub mutual_tls: bool, + pub identity: &'a Option, } impl<'a> ClusterConfig<'a> { @@ -237,6 +239,7 @@ impl<'a> ClusterConfig<'a> { regex_parser_limit: general.regex_parser_limit, pub_sub_enabled: general.pub_sub_enabled(), mutual_tls: config.general.tls_client_validate_cn, + identity: &user.identity, } } } @@ -283,6 +286,7 @@ impl Cluster { regex_parser_limit, pub_sub_enabled, mutual_tls, + identity, } = config; let identifier = Arc::new(DatabaseUser { @@ -343,6 +347,7 @@ impl Cluster { ), regex_parser: RegexParser::new(regex_parser_limit, query_parser), mutual_tls, + identity: identity.clone(), } } @@ -405,6 +410,12 @@ impl Cluster { &self.passwords } + /// Get user identity which should match the TLS certificate it provided + /// when connecting. + pub fn identity(&self) -> Option<&str> { + self.identity.as_deref() + } + /// User name. pub fn user(&self) -> &str { &self.identifier.user diff --git a/pgdog/src/frontend/client/mod.rs b/pgdog/src/frontend/client/mod.rs index d9a3ba6d9..dc7064893 100644 --- a/pgdog/src/frontend/client/mod.rs +++ b/pgdog/src/frontend/client/mod.rs @@ -182,12 +182,12 @@ impl Client { } } else if validate_cn { // This checks that the certificate CN (common name) - // matches the user name exactly. If the client is not connecting with TLS, + // matches the user identity exactly. If the client is not connecting with TLS, // this will fail. // // This is part of our mTLS implementation. // - stream.tls_cn() == Some(user) + stream.tls_cn() == databases::databases().identity((user, database)) } else { let passwords = if admin { Some(vec![PasswordKind::Plain(admin_password.clone())])