Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .schema/users.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
28 changes: 15 additions & 13 deletions integration/tls/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,39 +21,39 @@ 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
echo "FAIL (expected success)"
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
echo "FAIL (expected success)"
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
echo "OK (rejected)"
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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion integration/tls/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions integration/tls/users.toml
Original file line number Diff line number Diff line change
@@ -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"
11 changes: 8 additions & 3 deletions pgdog-config/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
Expand All @@ -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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the line below feel related enough to justify encapsulating in a method

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I don't even know if it's right though yet, I just added this to make the warning go away. Need to 🤔 🤔 🤔 🤔 🤔

&& 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
Expand Down Expand Up @@ -192,6 +195,8 @@ impl Display for PasswordKind {
)]
#[serde(deny_unknown_fields)]
pub struct User {
/// User identity used for mTLS.
pub identity: Option<String>,
/// Name of the user. Clients that connect to PgDog will need to use this username.
///
/// https://docs.pgdog.dev/configuration/users.toml/users/#name
Expand Down
7 changes: 7 additions & 0 deletions pgdog/src/backend/databases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Cluster, Error> {
let user = user.to_user();
Expand Down
11 changes: 11 additions & 0 deletions pgdog/src/backend/pool/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub struct Cluster {
resharding_replication_retry_min_delay: Duration,
regex_parser: RegexParser,
mutual_tls: bool,
identity: Option<String>,
}

/// Sharding configuration from the cluster.
Expand Down Expand Up @@ -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<String>,
}

impl<'a> ClusterConfig<'a> {
Expand Down Expand Up @@ -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,
}
}
}
Expand Down Expand Up @@ -283,6 +286,7 @@ impl Cluster {
regex_parser_limit,
pub_sub_enabled,
mutual_tls,
identity,
} = config;

let identifier = Arc::new(DatabaseUser {
Expand Down Expand Up @@ -343,6 +347,7 @@ impl Cluster {
),
regex_parser: RegexParser::new(regex_parser_limit, query_parser),
mutual_tls,
identity: identity.clone(),
}
}

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pgdog/src/frontend/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())])
Expand Down
Loading