From 56bb8628ec9f025694cc4bfb3ec68147eeb55317 Mon Sep 17 00:00:00 2001 From: Timon Borter Date: Fri, 5 Jul 2024 15:52:52 +0200 Subject: [PATCH] feat: postgresql user password rotation --- Cargo.toml | 1 + DEVELOPMENT.md | 21 ++++++++++++++++-- README.md | 4 +++- dev/config.yml | 4 +++- dev/docker-compose.yml | 1 + dev/podman.sh | 1 + dev/postgres/init.sql | 2 ++ src/config.rs | 4 +++- src/database.rs | 30 +++++++++++++++++++++++++ src/main.rs | 1 + src/workflow.rs | 50 ++++++++++++++++++++++++++++++------------ 11 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 dev/postgres/init.sql create mode 100644 src/database.rs diff --git a/Cargo.toml b/Cargo.toml index c55a59c..869f4f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ serde_yaml = "0.9.34+deprecated" tokio = { version = "1.38.0", features = ["macros", "rt"] } vaultrs = "0.7.2" rand = "0.9.0-alpha.1" +postgres = "0.19.7" [dev-dependencies] assert_cmd = "2.0.14" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 74b92b8..46091bf 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -52,8 +52,25 @@ npm ci --cache .npm - One to simulate the database of an application, used for secret rotation - **A Vault instance:** For managing secrets -Note that if using any of the below options, Vault will be accessible on http://localhost:8200. -The root token for development is 'root-token'. +Two options are provided for setting up the environment, either using `podman` or `docker-compose`. +Refer to the respective scripts ([`dev/podman.sh`](dev/podman.sh) and [`dev/docker-compose.yml`](dev/docker-compose.yml)) for detailed instructions. + +**Notes:** + +- If using any of these options, Vault will be accessible on http://localhost:8200. +- The provided "root-token" is for development only. Use strong, unique tokens in production and follow best practices for Vault token management. +- The demo database is initialized with sample users and credentials for demonstration purposes. After [having initialized Vault](#running-the-cli), you could configure these users for rotation, e.g. with the following secret value in `path/to/my/secret`: + +```json +{ + "postgresql_active_user": "user1", + "postgresql_active_user_password": "initialpw", + "postgresql_user_1": "user1", + "postgresql_user_1_password": "initialpw", + "postgresql_user_2": "user2", + "postgresql_user_2_password": "initialpw" +} +``` ### Setting up with `podman`: diff --git a/README.md b/README.md index 97d8d55..5279832 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ The configuration file is in YAML format and has the following structure: ```yaml postgres: - jdbc_url: 'jdbc:postgres://localhost:5432/demo' # Replace with your database URL + host: 'localhost' # Replace with your database host + port: 5432 # Replace with your database port + database: 'demo' # Replace with your database vault: address: 'http://localhost:8200' # Replace with your Vault address path: 'path/to/my/secret' # Replace with the desired path in Vault diff --git a/dev/config.yml b/dev/config.yml index 820ec71..0057483 100644 --- a/dev/config.yml +++ b/dev/config.yml @@ -1,5 +1,7 @@ postgres: - jdbc_url: 'jdbc:postgres://localhost:5432/demo' + host: 'localhost' + port: 5432 + database: 'demo' vault: address: 'http://localhost:8200' path: 'path/to/my/secret' diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 3f6012c..d5c028c 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -26,6 +26,7 @@ services: - '5432:5432' volumes: - demo-data:/var/lib/postgresql/data + - ./postgres:/docker-entrypoint-initdb.d # Vault server vault: diff --git a/dev/podman.sh b/dev/podman.sh index b8bb016..9e59533 100755 --- a/dev/podman.sh +++ b/dev/podman.sh @@ -19,6 +19,7 @@ podman start postgres-vault || \ podman start postgres-demo || \ podman run -d --name postgres-demo \ -v "${DEMO_DATA_VOLUME}:/var/lib/postgresql/data" \ + -v "$(dirname "$0")/postgres:/docker-entrypoint-initdb.d" \ -e POSTGRES_DB=demo \ -e POSTGRES_USER=demo \ -e POSTGRES_PASSWORD=demo_password \ diff --git a/dev/postgres/init.sql b/dev/postgres/init.sql new file mode 100644 index 0000000..fd21127 --- /dev/null +++ b/dev/postgres/init.sql @@ -0,0 +1,2 @@ +CREATE USER user1 WITH PASSWORD 'initialpw'; +CREATE USER user2 WITH PASSWORD 'initialpw'; diff --git a/src/config.rs b/src/config.rs index 7946e04..89b4040 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,7 +16,9 @@ pub(crate) struct VaultConfig { #[derive(Clone, Deserialize, Debug)] pub(crate) struct PostgresConfig { - pub(crate) jdbc_url: String, + pub(crate) host: String, + pub(crate) port: u16, + pub(crate) database: String, } pub(crate) fn read_config(config_path: PathBuf) -> Config { diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..ab75014 --- /dev/null +++ b/src/database.rs @@ -0,0 +1,30 @@ +use postgres::{Client, NoTls}; + +use crate::config::{Config, PostgresConfig}; + +pub struct PostgresClient { + postgres_config: PostgresConfig, +} + +impl PostgresClient { + pub(crate) fn init(config: &Config) -> PostgresClient { + PostgresClient { + postgres_config: config.postgres.clone(), + } + } + + pub(crate) fn connect_for_user(&self, username: String, password: String) -> Client { + let host = self.postgres_config.host.as_str(); + let port = self.postgres_config.port; + let database = self.postgres_config.database.as_str(); + + Client::connect( + format!( + "host={host} port={port} dbname={database} user={username} password={password}" + ) + .as_str(), + NoTls, + ) + .expect("Failed to build PostgreSQL connection") + } +} diff --git a/src/main.rs b/src/main.rs index 4451a9f..564c018 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use crate::workflow::rotate_secrets_using_switch_method; mod cli; mod config; +mod database; mod password; mod vault; mod workflow; diff --git a/src/workflow.rs b/src/workflow.rs index 1a851b1..1203efa 100644 --- a/src/workflow.rs +++ b/src/workflow.rs @@ -1,15 +1,21 @@ +use std::fmt::format; + +use log::debug; +use vaultrs::auth::userpass::user::update_password; + use crate::cli::RotateArgs; use crate::config::Config; +use crate::database::PostgresClient; use crate::password::generate_random_password; use crate::vault::{Vault, VaultStructure}; -use log::debug; -use vaultrs::auth::userpass::user::update_password; pub(crate) fn rotate_secrets_using_switch_method( rotate_args: &RotateArgs, config: &Config, vault: &mut Vault, ) { + let db: PostgresClient = PostgresClient::init(config); + debug!("Starting 'switch' workflow"); let vault_path = config.vault.clone().path; @@ -25,9 +31,7 @@ pub(crate) fn rotate_secrets_using_switch_method( let new_password: String = generate_random_password(rotate_args.password_length); - // TODO: PostgreSQL password change - - update_passive_user_password(&mut secret, new_password); + update_passive_user_password(&db, &mut secret, new_password); switch_active_user(&mut secret); vault @@ -38,9 +42,7 @@ pub(crate) fn rotate_secrets_using_switch_method( let new_password: String = generate_random_password(rotate_args.password_length); - // TODO: PostgreSQL password change - - update_passive_user_password(&mut secret, new_password); + update_passive_user_password(&db, &mut secret, new_password); vault .write_secret(&secret) .expect("Failed to update PASSIVE user password after sync"); @@ -58,12 +60,32 @@ fn switch_active_user(secret: &mut VaultStructure) { } } -fn update_passive_user_password(secret: &mut VaultStructure, new_password: String) { - if secret.postgresql_active_user == secret.postgresql_user_1 { - secret.postgresql_user_2_password = new_password.clone(); - } else { - secret.postgresql_user_1_password = new_password.clone(); - } +fn update_passive_user_password( + db: &PostgresClient, + secret: &mut VaultStructure, + new_password: String, +) { + let (passive_user, passive_user_password) = + if secret.postgresql_active_user == secret.postgresql_user_1 { + secret.postgresql_user_2_password = new_password.clone(); + ( + secret.postgresql_user_2.clone(), + secret.postgresql_user_2_password.clone(), + ) + } else { + secret.postgresql_user_1_password = new_password.clone(); + ( + secret.postgresql_user_1.clone(), + secret.postgresql_user_1_password.clone(), + ) + }; + + let mut conn = db.connect_for_user(passive_user.clone(), passive_user_password); + let query = + "ALTER ROLE $1 WITH PASSWORD '$2'"; + + conn.execute(query, &[&passive_user, &new_password]) + .expect(format!("Failed to update password of '{passive_user}'").as_str()); } mod tests {