Skip to content

Zero downtime password rotation #389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load bal
| Automatic sharding | **Experimental** | PgCat can parse queries, detect sharding keys automatically, and route queries to the correct shard. |
| Mirroring | **Experimental** | Mirror queries between multiple databases in order to test servers with realistic production traffic. |
| Auth passthrough | **Experimental** | MD5 password authentication can be configured to use an `auth_query` so no cleartext passwords are needed in the config file. |
| Password rotation | **Experimental** | Allows to rotate passwords without downtime or using third-party tools to manage Postgres authentication. |


## Status
Expand Down Expand Up @@ -244,6 +245,12 @@ The config can be reloaded by sending a `kill -s SIGHUP` to the process or by qu

Mirroring allows to route queries to multiple databases at the same time. This is useful for prewarning replicas before placing them into the active configuration, or for testing different versions of Postgres with live traffic.

### Password rotation

Password rotation allows to specify multiple passwords for a user, so they can connect to PgCat with multiple credentials. This allows distributed applications to change their configuration (connection strings) gradually and for PgCat to monitor their progression in admin statistics. Once the new secret is deployed everywhere, the old one can be removed from PgCat.

This also decouples server passwords from client passwords, allowing to change one without necessarily changing the other.

## License

PgCat is free and open source, released under the MIT license.
Expand Down
8 changes: 6 additions & 2 deletions pgcat.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ tcp_keepalives_count = 5
tcp_keepalives_interval = 5

# Path to TLS Certficate file to use for TLS connections
# tls_certificate = "server.cert"
# tls_certificate = ".circleci/server.cert"
# Path to TLS private key file to use for TLS connections
# tls_private_key = "server.key"
# tls_private_key = ".circleci/server.key"

# User name to access the virtual administrative database (pgbouncer or pgcat)
# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc..
Expand Down Expand Up @@ -129,6 +129,10 @@ connect_timeout = 3000
username = "sharding_user"
# Postgresql password
password = "sharding_user"

# Passwords the client can use to connect. Useful for password rotations.
secrets = [ "secret_one", "secret_two" ]

# Maximum number of server connections that can be established for this user
# The maximum number of connection from a single Pgcat process to any database in the cluster
# is the sum of pool_size across all users.
Expand Down
15 changes: 12 additions & 3 deletions src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ where
let columns = vec![
("database", DataType::Text),
("user", DataType::Text),
("secret", DataType::Text),
("pool_mode", DataType::Text),
("cl_idle", DataType::Numeric),
("cl_active", DataType::Numeric),
Expand All @@ -276,10 +277,11 @@ where
let mut res = BytesMut::new();
res.put(row_description(&columns));

for ((_user_pool, _pool), pool_stats) in all_pool_stats {
for (_, pool_stats) in all_pool_stats {
let mut row = vec![
pool_stats.database(),
pool_stats.user(),
pool_stats.redacted_secret(),
pool_stats.pool_mode().to_string(),
];
pool_stats.populate_row(&mut row);
Expand Down Expand Up @@ -780,7 +782,7 @@ where
let database = parts[0];
let user = parts[1];

match get_pool(database, user) {
match get_pool(database, user, None) {
Some(pool) => {
pool.pause();

Expand Down Expand Up @@ -827,7 +829,7 @@ where
let database = parts[0];
let user = parts[1];

match get_pool(database, user) {
match get_pool(database, user, None) {
Some(pool) => {
pool.resume();

Expand Down Expand Up @@ -895,13 +897,20 @@ where
res.put(row_description(&vec![
("name", DataType::Text),
("pool_mode", DataType::Text),
("secret", DataType::Text),
]));

for (user_pool, pool) in get_all_pools() {
let pool_config = &pool.settings;
let redacted_secret = match user_pool.secret {
Some(secret) => format!("****{}", &secret[secret.len() - 4..]),
None => "<no secret>".to_string(),
};

res.put(data_row(&vec![
user_pool.user.clone(),
pool_config.pool_mode.to_string(),
redacted_secret,
]));
}

Expand Down
Loading