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(); + } +}