Skip to content
Open
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
629 changes: 601 additions & 28 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 @@ -120,6 +120,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
Expand Down Expand Up @@ -197,6 +199,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
Expand Down
6 changes: 6 additions & 0 deletions example.users.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,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
84 changes: 78 additions & 6 deletions pgdog-config/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,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 @@ -57,8 +57,7 @@ impl ConfigAndUsers {
}

let mut users: Users = if let Ok(users) = read_to_string(users_path) {
let mut users: Users = toml::from_str(&users)?;
users.check(&config);
let users: Users = toml::from_str(&users)?;
info!("loaded \"{}\"", users_path.display());
users
} else {
Expand All @@ -82,12 +81,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 @@ -1179,4 +1215,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
56 changes: 56 additions & 0 deletions pgdog-config/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ impl Users {
}
}

/// Backend authentication mode used by PgDog for server connections.
#[derive(
Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd, Hash,
)]
#[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.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, Ord, PartialOrd)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -104,6 +123,11 @@ pub struct User {
pub server_user: Option<String>,
/// 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.
pub statement_timeout: Option<u64>,
/// Relication mode.
Expand Down Expand Up @@ -253,4 +277,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"));
}
}
2 changes: 2 additions & 0 deletions pgdog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ hickory-resolver = "0.25.2"
lazy_static = "1"
dashmap = "6"
derive_builder = "0.20.2"
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-rds = "1"
pgdog-config = { path = "../pgdog-config" }
pgdog-vector = { path = "../pgdog-vector" }
pgdog-stats = { path = "../pgdog-stats" }
Expand Down
1 change: 1 addition & 0 deletions pgdog/src/backend/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod rds_iam;
Loading