Skip to content

Commit 489e4d1

Browse files
committed
feat: postgresql user password rotation
1 parent 023ce6f commit 489e4d1

11 files changed

+103
-20
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ serde_yaml = "0.9.34+deprecated"
1313
tokio = { version = "1.38.0", features = ["macros", "rt"] }
1414
vaultrs = "0.7.2"
1515
rand = "0.9.0-alpha.1"
16+
postgres = "0.19.7"
1617

1718
[dev-dependencies]
1819
assert_cmd = "2.0.14"

DEVELOPMENT.md

+19-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,25 @@ npm ci --cache .npm
5252
- One to simulate the database of an application, used for secret rotation
5353
- **A Vault instance:** For managing secrets
5454

55-
Note that if using any of the below options, Vault will be accessible on http://localhost:8200.
56-
The root token for development is 'root-token'.
55+
Two options are provided for setting up the environment, either using `podman` or `docker-compose`.
56+
Refer to the respective scripts ([`dev/podman.sh`](dev/podman.sh) and [`dev/docker-compose.yml`](dev/docker-compose.yml)) for detailed instructions.
57+
58+
**Notes:**
59+
60+
- If using any of these options, Vault will be accessible on http://localhost:8200.
61+
- The provided "root-token" is for development only. Use strong, unique tokens in production and follow best practices for Vault token management.
62+
- 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`:
63+
64+
```json
65+
{
66+
"postgresql_active_user": "user1",
67+
"postgresql_active_user_password": "initialpw",
68+
"postgresql_user_1": "user1",
69+
"postgresql_user_1_password": "initialpw",
70+
"postgresql_user_2": "user2",
71+
"postgresql_user_2_password": "initialpw"
72+
}
73+
```
5774

5875
### Setting up with `podman`:
5976

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ The configuration file is in YAML format and has the following structure:
3838

3939
```yaml
4040
postgres:
41-
jdbc_url: 'jdbc:postgres://localhost:5432/demo' # Replace with your database URL
41+
host: 'localhost' # Replace with your database host
42+
port: 5432 # Replace with your database port
43+
database: 'demo' # Replace with your database
4244
vault:
4345
address: 'http://localhost:8200' # Replace with your Vault address
4446
path: 'path/to/my/secret' # Replace with the desired path in Vault

dev/config.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
postgres:
2-
jdbc_url: 'jdbc:postgres://localhost:5432/demo'
2+
host: 'localhost'
3+
port: 5432
4+
database: 'demo'
35
vault:
46
address: 'http://localhost:8200'
57
path: 'path/to/my/secret'

dev/docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ services:
2626
- '5432:5432'
2727
volumes:
2828
- demo-data:/var/lib/postgresql/data
29+
- ./postgres:/docker-entrypoint-initdb.d
2930

3031
# Vault server
3132
vault:

dev/podman.sh

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ podman start postgres-vault || \
1919
podman start postgres-demo || \
2020
podman run -d --name postgres-demo \
2121
-v "${DEMO_DATA_VOLUME}:/var/lib/postgresql/data" \
22+
-v "$(dirname "$0")/postgres:/docker-entrypoint-initdb.d" \
2223
-e POSTGRES_DB=demo \
2324
-e POSTGRES_USER=demo \
2425
-e POSTGRES_PASSWORD=demo_password \

dev/postgres/init.sql

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE USER user1 WITH PASSWORD 'initialpw';
2+
CREATE USER user2 WITH PASSWORD 'initialpw';

src/config.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ pub(crate) struct VaultConfig {
1616

1717
#[derive(Clone, Deserialize, Debug)]
1818
pub(crate) struct PostgresConfig {
19-
pub(crate) jdbc_url: String,
19+
pub(crate) host: String,
20+
pub(crate) port: u16,
21+
pub(crate) database: String,
2022
}
2123

2224
pub(crate) fn read_config(config_path: PathBuf) -> Config {

src/database.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use postgres::{Client, NoTls};
2+
3+
use crate::config::{Config, PostgresConfig};
4+
5+
pub struct PostgresClient {
6+
postgres_config: PostgresConfig,
7+
}
8+
9+
impl PostgresClient {
10+
pub(crate) fn init(config: &Config) -> PostgresClient {
11+
PostgresClient {
12+
postgres_config: config.postgres.clone(),
13+
}
14+
}
15+
16+
pub(crate) fn connect_for_user(&self, username: String, password: String) -> Client {
17+
let host = self.postgres_config.host.as_str();
18+
let port = self.postgres_config.port;
19+
let database = self.postgres_config.database.as_str();
20+
21+
Client::connect(
22+
format!(
23+
"host={host} port={port} dbname={database} user={username} password={password}"
24+
)
25+
.as_str(),
26+
NoTls,
27+
)
28+
.expect("Failed to build PostgreSQL connection")
29+
}
30+
}

src/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::workflow::rotate_secrets_using_switch_method;
1010

1111
mod cli;
1212
mod config;
13+
mod database;
1314
mod password;
1415
mod vault;
1516
mod workflow;

src/workflow.rs

+39-15
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
use std::fmt::format;
2+
3+
use log::{debug, trace};
4+
use vaultrs::auth::userpass::user::update_password;
5+
16
use crate::cli::RotateArgs;
27
use crate::config::Config;
8+
use crate::database::PostgresClient;
39
use crate::password::generate_random_password;
410
use crate::vault::{Vault, VaultStructure};
5-
use log::debug;
6-
use vaultrs::auth::userpass::user::update_password;
711

812
pub(crate) fn rotate_secrets_using_switch_method(
913
rotate_args: &RotateArgs,
1014
config: &Config,
1115
vault: &mut Vault,
1216
) {
17+
let db: PostgresClient = PostgresClient::init(config);
18+
1319
debug!("Starting 'switch' workflow");
1420

1521
let vault_path = config.vault.clone().path;
@@ -25,25 +31,24 @@ pub(crate) fn rotate_secrets_using_switch_method(
2531

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

28-
// TODO: PostgreSQL password change
29-
30-
update_passive_user_password(&mut secret, new_password);
34+
update_passive_user_password(&db, &mut secret, new_password);
3135
switch_active_user(&mut secret);
3236

3337
vault
3438
.write_secret(&secret)
35-
.expect("Failed to kick-off rotation workflow by switching active user");
39+
.expect("Failed to kick-off rotation workflow by switching active user - Vault is in an invalid state");
40+
41+
debug!("Active and passive users switched and synchronized into Vault");
3642

3743
// TODO: Trigger ArgoCD Sync
3844

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

41-
// TODO: PostgreSQL password change
47+
update_passive_user_password(&db, &mut secret, new_password);
4248

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

4853
println!("Successfully rotated all secrets")
4954
}
@@ -56,14 +61,33 @@ fn switch_active_user(secret: &mut VaultStructure) {
5661
secret.postgresql_active_user = secret.postgresql_user_1.clone();
5762
secret.postgresql_active_user_password = secret.postgresql_user_1_password.clone()
5863
}
64+
65+
trace!("Switched active and passive user in Vault secret (locally)")
5966
}
6067

61-
fn update_passive_user_password(secret: &mut VaultStructure, new_password: String) {
62-
if secret.postgresql_active_user == secret.postgresql_user_1 {
63-
secret.postgresql_user_2_password = new_password.clone();
64-
} else {
65-
secret.postgresql_user_1_password = new_password.clone();
66-
}
68+
fn update_passive_user_password(
69+
db: &PostgresClient,
70+
secret: &mut VaultStructure,
71+
new_password: String,
72+
) {
73+
let (passive_user, passive_user_password) =
74+
if secret.postgresql_active_user == secret.postgresql_user_1 {
75+
let original_password = secret.postgresql_user_2_password.clone();
76+
secret.postgresql_user_2_password = new_password.clone();
77+
(secret.postgresql_user_2.clone(), original_password)
78+
} else {
79+
let original_password = secret.postgresql_user_1_password.clone();
80+
secret.postgresql_user_1_password = new_password.clone();
81+
(secret.postgresql_user_1.clone(), original_password)
82+
};
83+
84+
let mut conn = db.connect_for_user(passive_user.clone(), passive_user_password);
85+
let query = format!("ALTER ROLE {passive_user} WITH PASSWORD '{new_password}'");
86+
87+
conn.execute(query.as_str(), &[])
88+
.expect(format!("Failed to update password of '{passive_user}'").as_str());
89+
90+
debug!("Successfully rotated PostgreSQL password of passive user");
6791
}
6892

6993
mod tests {

0 commit comments

Comments
 (0)