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
627 changes: 600 additions & 27 deletions Cargo.lock

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ password = "hunter2"

If a database in `pgdog.toml` doesn't have a user in `users.toml`, the connection pool for that database will not be created and users won't be able to connect.

### RDS IAM backend authentication

PgDog can keep client-to-proxy authentication unchanged while using AWS RDS IAM tokens for proxy-to-PostgreSQL authentication on a per-user basis.

```toml
[[users]]
name = "alice"
database = "pgdog"
password = "client-password"
server_auth = "rds_iam"
# Optional; PgDog infers region from *.region.rds.amazonaws.com(.cn) hostnames when omitted.
# server_iam_region = "us-east-1"
```

When any user has `server_auth = "rds_iam"`:

- `general.tls_verify` must not be `"disabled"`.
- `general.passthrough_auth` must be `"disabled"`.

If you'd like to try it out locally, create the database and user like so:

```sql
Expand Down
4 changes: 4 additions & 0 deletions example.pgdog.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ tls_client_required = false
# - prefer
# - verify-ca
# - verify-full
# NOTE: if any user sets `server_auth = "rds_iam"` in users.toml,
# this cannot be "disabled".
tls_verify = "disabled"
# Path to PEM-encoded certificate bundle to use for Postgres server
# certificate validation.
Expand Down Expand Up @@ -170,6 +172,8 @@ query_cache_limit = 1_000
# - enabled (requires TLS)
# - enabled_plain
#
# NOTE: `passthrough_auth` cannot be enabled when using
# per-user backend `server_auth = "rds_iam"`.
passthrough_auth = "disabled"
# How long to wait for Postgres server connections to be created by the pool before
# returning an error and banning the database.
Expand Down
6 changes: 6 additions & 0 deletions example.users.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ password = "pgdog"
name = "pgdog"
database = "pgdog_sharded"
password = "pgdog"

# Example: backend authentication with AWS RDS IAM token generation.
# PgDog still authenticates the client as configured by `general.auth_type`;
# this only affects how PgDog authenticates to PostgreSQL servers.
# server_auth = "rds_iam"
# server_iam_region = "us-east-1" # optional; auto-inferred from RDS hostname when omitted
81 changes: 77 additions & 4 deletions pgdog-config/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ use crate::{
use super::database::Database;
use super::error::Error;
use super::general::General;
use super::networking::{MultiTenant, Tcp};
use super::networking::{MultiTenant, Tcp, TlsVerifyMode};
use super::pooling::PoolerMode;
use super::replication::{MirrorConfig, Mirroring, ReplicaLag, Replication};
use super::rewrite::Rewrite;
use super::sharding::{ManualQuery, OmnishardedTables, ShardedMapping, ShardedTable};
use super::users::{Admin, Plugin, Users};
use super::users::{Admin, Plugin, ServerAuth, Users};

#[derive(Debug, Clone, PartialEq)]
pub struct ConfigAndUsers {
Expand Down Expand Up @@ -94,12 +94,49 @@ impl ConfigAndUsers {
warn!("admin password has been randomly generated");
}

Ok(ConfigAndUsers {
let mut config_and_users = ConfigAndUsers {
config,
users,
config_path: config_path.to_owned(),
users_path: users_path.to_owned(),
})
};

config_and_users.check()?;

Ok(config_and_users)
}

pub fn check(&mut self) -> Result<(), Error> {
self.config.check();
self.users.check(&self.config);
self.validate_server_auth()?;
Ok(())
}

fn validate_server_auth(&self) -> Result<(), Error> {
let has_rds_iam_user = self
.users
.users
.iter()
.any(|user| user.server_auth == ServerAuth::RdsIam);

if !has_rds_iam_user {
return Ok(());
}

if self.config.general.passthrough_auth != PassthoughAuth::Disabled {
return Err(Error::ParseError(
"\"passthrough_auth\" must be \"disabled\" when any user has \"server_auth = \\\"rds_iam\\\"\"".into(),
));
}

if self.config.general.tls_verify == TlsVerifyMode::Disabled {
return Err(Error::ParseError(
"\"tls_verify\" cannot be \"disabled\" when any user has \"server_auth = \\\"rds_iam\\\"\"".into(),
));
}

Ok(())
}

/// Prepared statements are enabled.
Expand Down Expand Up @@ -1223,4 +1260,40 @@ shard = 0
assert_eq!(dest.host, "source-host");
assert_eq!(dest.port, 5432);
}

#[test]
fn test_rds_iam_rejects_passthrough_auth() {
let mut config = ConfigAndUsers::default();
config.config.general.passthrough_auth = PassthoughAuth::EnabledPlain;
config.config.general.tls_verify = TlsVerifyMode::VerifyFull;
config.users.users.push(crate::User {
name: "alice".into(),
database: "db".into(),
password: Some("secret".into()),
server_auth: ServerAuth::RdsIam,
..Default::default()
});

let err = config.check().unwrap_err().to_string();
assert!(err.contains("passthrough_auth"));
assert!(err.contains("rds_iam"));
}

#[test]
fn test_rds_iam_rejects_tls_verify_disabled() {
let mut config = ConfigAndUsers::default();
config.config.general.tls_verify = TlsVerifyMode::Disabled;
config.config.general.passthrough_auth = PassthoughAuth::Disabled;
config.users.users.push(crate::User {
name: "alice".into(),
database: "db".into(),
password: Some("secret".into()),
server_auth: ServerAuth::RdsIam,
..Default::default()
});

let err = config.check().unwrap_err().to_string();
assert!(err.contains("tls_verify"));
assert!(err.contains("rds_iam"));
}
}
2 changes: 1 addition & 1 deletion pgdog-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub use replication::*;
pub use rewrite::{Rewrite, RewriteMode};
pub use sharding::*;
pub use system_catalogs::system_catalogs;
pub use users::{Admin, Plugin, User, Users};
pub use users::{Admin, Plugin, ServerAuth, User, Users};

use std::time::Duration;

Expand Down
70 changes: 70 additions & 0 deletions pgdog-config/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,37 @@ impl Users {
}
}

/// Backend authentication mode used by PgDog for server connections.
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
Ord,
PartialOrd,
Hash,
JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ServerAuth {
/// Use configured static password.
#[default]
Password,
/// Generate an AWS RDS IAM auth token per connection attempt.
RdsIam,
}

impl ServerAuth {
pub fn rds_iam(&self) -> bool {
matches!(self, Self::RdsIam)
}
}

/// User allowed to connect to pgDog.
/// A user entry in `users.toml`, controlling which users are allowed to connect to PgDog.
///
/// https://docs.pgdog.dev/configuration/users.toml/users/
Expand Down Expand Up @@ -142,6 +173,13 @@ pub struct User {
///
/// https://docs.pgdog.dev/configuration/users.toml/users/#server_password
pub server_password: Option<String>,
/// Backend auth mode for server connections.
#[serde(default)]
pub server_auth: ServerAuth,
/// Optional region override for RDS IAM token generation.
pub server_iam_region: Option<String>,
/// Statement timeout.
///
/// Sets the `statement_timeout` on all server connections at connection creation. This allows you to set a reasonable default for each user without modifying `postgresql.conf` or using `ALTER USER`.
///
/// **Note:** Nothing is preventing the user from manually changing this setting at runtime, e.g., by running `SET statement_timeout TO 0`;
Expand Down Expand Up @@ -337,4 +375,36 @@ mod tests {
.unwrap();
assert_eq!(bob_source.password(), "pass4");
}

#[test]
fn test_user_server_auth_defaults_to_password() {
let source = r#"
[[users]]
name = "alice"
database = "db"
password = "secret"
"#;

let users: Users = toml::from_str(source).unwrap();
let user = users.users.first().unwrap();
assert_eq!(user.server_auth, ServerAuth::Password);
assert!(user.server_iam_region.is_none());
}

#[test]
fn test_user_server_auth_rds_iam_with_region() {
let source = r#"
[[users]]
name = "alice"
database = "db"
password = "secret"
server_auth = "rds_iam"
server_iam_region = "us-east-1"
"#;

let users: Users = toml::from_str(source).unwrap();
let user = users.users.first().unwrap();
assert_eq!(user.server_auth, ServerAuth::RdsIam);
assert_eq!(user.server_iam_region.as_deref(), Some("us-east-1"));
}
}
Loading
Loading