Skip to content

Commit

Permalink
feat: postgresql user password rotation
Browse files Browse the repository at this point in the history
  • Loading branch information
bbortt committed Jul 7, 2024
1 parent 023ce6f commit 489e4d1
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 20 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
21 changes: 19 additions & 2 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion dev/config.yml
Original file line number Diff line number Diff line change
@@ -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'
1 change: 1 addition & 0 deletions dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ services:
- '5432:5432'
volumes:
- demo-data:/var/lib/postgresql/data
- ./postgres:/docker-entrypoint-initdb.d

# Vault server
vault:
Expand Down
1 change: 1 addition & 0 deletions dev/podman.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 2 additions & 0 deletions dev/postgres/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CREATE USER user1 WITH PASSWORD 'initialpw';
CREATE USER user2 WITH PASSWORD 'initialpw';
4 changes: 3 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
30 changes: 30 additions & 0 deletions src/database.rs
Original file line number Diff line number Diff line change
@@ -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")
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::workflow::rotate_secrets_using_switch_method;

mod cli;
mod config;
mod database;
mod password;
mod vault;
mod workflow;
Expand Down
54 changes: 39 additions & 15 deletions src/workflow.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
use std::fmt::format;

Check warning on line 1 in src/workflow.rs

View workflow job for this annotation

GitHub Actions / Rust Build

unused import: `std::fmt::format`

Check warning on line 1 in src/workflow.rs

View workflow job for this annotation

GitHub Actions / Rust Build

unused import: `std::fmt::format`

use log::{debug, trace};
use vaultrs::auth::userpass::user::update_password;

Check warning on line 4 in src/workflow.rs

View workflow job for this annotation

GitHub Actions / Rust Build

unused import: `vaultrs::auth::userpass::user::update_password`

Check warning on line 4 in src/workflow.rs

View workflow job for this annotation

GitHub Actions / Rust Build

unused import: `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;
Expand All @@ -25,25 +31,24 @@ 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
.write_secret(&secret)
.expect("Failed to kick-off rotation workflow by switching active user");
.expect("Failed to kick-off rotation workflow by switching active user - Vault is in an invalid state");

debug!("Active and passive users switched and synchronized into Vault");

// TODO: Trigger ArgoCD Sync

let new_password: String = generate_random_password(rotate_args.password_length);

// TODO: PostgreSQL password change
update_passive_user_password(&db, &mut secret, new_password);

update_passive_user_password(&mut secret, new_password);
vault
.write_secret(&secret)
.expect("Failed to update PASSIVE user password after sync");
.expect("Failed to update PASSIVE user password after sync - Vault is in an invalid state");

println!("Successfully rotated all secrets")
}
Expand All @@ -56,14 +61,33 @@ fn switch_active_user(secret: &mut VaultStructure) {
secret.postgresql_active_user = secret.postgresql_user_1.clone();
secret.postgresql_active_user_password = secret.postgresql_user_1_password.clone()
}

trace!("Switched active and passive user in Vault secret (locally)")
}

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 {
let original_password = secret.postgresql_user_2_password.clone();
secret.postgresql_user_2_password = new_password.clone();
(secret.postgresql_user_2.clone(), original_password)
} else {
let original_password = secret.postgresql_user_1_password.clone();
secret.postgresql_user_1_password = new_password.clone();
(secret.postgresql_user_1.clone(), original_password)
};

let mut conn = db.connect_for_user(passive_user.clone(), passive_user_password);
let query = format!("ALTER ROLE {passive_user} WITH PASSWORD '{new_password}'");

conn.execute(query.as_str(), &[])
.expect(format!("Failed to update password of '{passive_user}'").as_str());

debug!("Successfully rotated PostgreSQL password of passive user");
}

mod tests {
Expand Down

0 comments on commit 489e4d1

Please sign in to comment.