diff --git a/.idea/runConfigurations/Run_propeller__init_vault.xml b/.idea/runConfigurations/Run_propeller__init_vault.xml
new file mode 100644
index 0000000..0445d65
--- /dev/null
+++ b/.idea/runConfigurations/Run_propeller__init_vault.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run_propeller__rotate.xml b/.idea/runConfigurations/Run_propeller__rotate.xml
new file mode 100644
index 0000000..26ebce3
--- /dev/null
+++ b/.idea/runConfigurations/Run_propeller__rotate.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/cli.rs b/src/cli.rs
index 1f1ff15..d77ce7d 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -17,13 +17,11 @@ pub(crate) enum Command {
/// Initialize a Vault path with the necessary structure for secret management.
///
/// This command prepares the Vault backend for subsequent secret rotation operations.
- #[command(arg_required_else_help(true))] // Require arguments for this subcommand
InitVault(InitVaultArgs),
/// Rotate PostgreSQL database secrets.
///
/// This command orchestrates the process of generating new secrets, updating the database, and storing the new secrets in Vault.
- #[command(arg_required_else_help(true))] // Require arguments for this subcommand
Rotate(RotateArgs),
}
@@ -43,7 +41,7 @@ pub(crate) struct RotateArgs {
/// Whether the CLI should write a recovery log (contains sensitive information!) or not
#[clap(short, long, default_value = "20")]
- pub(crate) password_length: i8,
+ pub(crate) password_length: usize,
/// Whether the CLI should write a recovery log (contains sensitive information!) or not
#[clap(short, long)]
diff --git a/src/config.rs b/src/config.rs
index 8b40491..bf27c7a 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -24,7 +24,8 @@ pub(crate) fn read_config(config_path: PathBuf) -> Config {
debug!("Reading config at: {path_string}");
let mut config_data: String = String::new();
- let mut config_file: File = File::open(config_path).expect("Failed to read configuration file");
+ let mut config_file: File = File::open(config_path)
+ .expect(format!("Failed to read configuration file: '{path_string}'").as_str());
config_file
.read_to_string(&mut config_data)
.expect("Failed to read configuration file");
diff --git a/src/main.rs b/src/main.rs
index 674ab40..4451a9f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,7 +6,7 @@ use config::Config;
use crate::cli::{CliArgs, Command};
use crate::config::read_config;
use crate::vault::Vault;
-use crate::workflow::switch_active_user;
+use crate::workflow::rotate_secrets_using_switch_method;
mod cli;
mod config;
@@ -21,14 +21,14 @@ fn main() {
match args.command {
Command::InitVault(int_args) => {
- let config: Config = read_config(int_args.base.config_path);
+ let config: Config = read_config(int_args.base.config_path.clone());
let mut vault: Vault = Vault::connect(&config);
vault.init_secret_path()
}
Command::Rotate(rotate_args) => {
- let config: Config = read_config(rotate_args.base.config_path);
- let vault: Vault = Vault::connect(&config);
- switch_active_user(&config, &vault)
+ let config: Config = read_config(rotate_args.base.config_path.clone());
+ let mut vault: Vault = Vault::connect(&config);
+ rotate_secrets_using_switch_method(&rotate_args, &config, &mut vault)
}
}
}
diff --git a/src/password.rs b/src/password.rs
index 26e8f16..d8b49ea 100644
--- a/src/password.rs
+++ b/src/password.rs
@@ -1,7 +1,7 @@
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
-fn generate_random_password(length: usize) -> String {
+pub(crate) fn generate_random_password(length: usize) -> String {
let mut rng = thread_rng();
let password: String = (0..length)
.map(|_| rng.sample(Alphanumeric) as char)
diff --git a/src/vault.rs b/src/vault.rs
index 49e3f9a..3991df4 100644
--- a/src/vault.rs
+++ b/src/vault.rs
@@ -1,8 +1,11 @@
use log::info;
+use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::env;
use tokio::runtime::{Builder, Runtime};
+use vaultrs::api::kv2::responses::SecretVersionMetadata;
use vaultrs::client::{VaultClient, VaultClientSettingsBuilder};
+use vaultrs::error::ClientError;
use vaultrs::kv2;
use crate::config::{Config, VaultConfig};
@@ -10,13 +13,13 @@ use crate::config::{Config, VaultConfig};
const VAULT_TOKEN: &'static str = "VAULT_TOKEN";
#[derive(Debug, Deserialize, Serialize)]
-struct VaultStructure {
- postgresql_active_user: String,
- postgresql_active_user_password: String,
- postgresql_user_1: String,
- postgresql_user_1_password: String,
- postgresql_user_2: String,
- postgresql_user_2_password: String,
+pub(crate) struct VaultStructure {
+ pub(crate) postgresql_active_user: String,
+ pub(crate) postgresql_active_user_password: String,
+ pub(crate) postgresql_user_1: String,
+ pub(crate) postgresql_user_1_password: String,
+ pub(crate) postgresql_user_2: String,
+ pub(crate) postgresql_user_2_password: String,
}
pub(crate) struct Vault {
@@ -51,13 +54,7 @@ impl Vault {
postgresql_user_2_password: "TBD".to_string(),
};
- self.rt
- .block_on(kv2::set(
- &self.vault_client,
- "secret",
- &*self.vault_config.path,
- &vault_structure,
- ))
+ self.write_secret(&vault_structure)
.expect("Failed to create initial Vault structure");
println!(
@@ -65,6 +62,26 @@ impl Vault {
self.vault_config.path
)
}
+
+ pub(crate) fn read_secret(&mut self) -> Result {
+ self.rt.block_on(kv2::read(
+ &self.vault_client,
+ "secret",
+ &*self.vault_config.path,
+ ))
+ }
+
+ pub(crate) fn write_secret(
+ &mut self,
+ vault_structure: &VaultStructure,
+ ) -> Result {
+ self.rt.block_on(kv2::set(
+ &self.vault_client,
+ "secret",
+ &*self.vault_config.path,
+ &vault_structure,
+ ))
+ }
}
fn get_vault_client(config: &Config) -> VaultClient {
diff --git a/src/workflow.rs b/src/workflow.rs
index b4aa382..0ede26a 100644
--- a/src/workflow.rs
+++ b/src/workflow.rs
@@ -1,4 +1,61 @@
+use crate::cli::RotateArgs;
use crate::config::Config;
-use crate::vault::Vault;
+use crate::password::generate_random_password;
+use crate::vault::{Vault, VaultStructure};
+use log::debug;
+use vaultrs::auth::userpass::user::update_password;
-pub(crate) fn switch_active_user(config: &Config, vault: &Vault) {}
+pub(crate) fn rotate_secrets_using_switch_method(
+ rotate_args: &RotateArgs,
+ config: &Config,
+ vault: &mut Vault,
+) {
+ debug!("Starting 'switch' workflow");
+
+ let vault_path = config.vault.clone().path;
+ let mut secret: VaultStructure = vault
+ .read_secret()
+ .expect(format!("Failed to read path '{vault_path}' - did you init Vault?").as_str());
+
+ if secret.postgresql_active_user != secret.postgresql_user_1
+ && secret.postgresql_active_user != secret.postgresql_user_2
+ {
+ panic!("Failed to detect active user - did neither match user 1 nor 2")
+ }
+
+ let new_password: String = generate_random_password(rotate_args.password_length);
+ update_passive_user_password(&mut secret, new_password);
+ switch_active_user(&mut secret);
+
+ vault
+ .write_secret(&secret)
+ .expect("Failed to kick-off rotation workflow by switching active user");
+
+ // TODO: Trigger ArgoCD Sync
+
+ let new_password: String = generate_random_password(rotate_args.password_length);
+ update_passive_user_password(&mut secret, new_password);
+ vault
+ .write_secret(&secret)
+ .expect("Failed to update PASSIVE user password after sync");
+
+ println!("Successfully rotated all secrets")
+}
+
+fn switch_active_user(secret: &mut VaultStructure) {
+ if secret.postgresql_active_user == secret.postgresql_user_1 {
+ secret.postgresql_active_user = secret.postgresql_user_2.clone();
+ secret.postgresql_active_user_password = secret.postgresql_user_2_password.clone()
+ } else {
+ secret.postgresql_active_user = secret.postgresql_user_1.clone();
+ secret.postgresql_active_user_password = secret.postgresql_user_1_password.clone()
+ }
+}
+
+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();
+ }
+}