From 3e1ddfc7b7141f0f82aeb9a25060945e817e7392 Mon Sep 17 00:00:00 2001 From: kelleyblackmore Date: Wed, 12 Nov 2025 08:26:58 +0000 Subject: [PATCH 1/5] added password gen, env updater, included in dev vault instance, .devcontainer --- .devcontainer/devcontainer.json | 19 ++- .devcontainer/install-vault.sh | 23 +++ Makefile | 197 ++++++++++++++++++++++++++ examples/config.toml | 3 +- examples/rotator-config.toml | 8 ++ src/env_updater.rs | 240 ++++++++++++++++++++++++++++++++ src/main.rs | 104 ++++++++++++++ src/vault.rs | 6 + 8 files changed, 598 insertions(+), 2 deletions(-) create mode 100755 .devcontainer/install-vault.sh create mode 100644 Makefile create mode 100644 examples/rotator-config.toml create mode 100644 src/env_updater.rs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 39bbd26..994d7aa 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,21 @@ { "image": "mcr.microsoft.com/devcontainers/universal:2", - "features": {} + "features": { + "ghcr.io/devcontainers/features/rust:1": { + "version": "latest", + "profile": "default" + }, + "ghcr.io/devcontainers-contrib/features/vault-asdf:2": { + "version": "latest" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer", + "serayuzgur.crates" + ] + } + }, + "postCreateCommand": "rustup component add rustfmt clippy && vault version" } diff --git a/.devcontainer/install-vault.sh b/.devcontainer/install-vault.sh new file mode 100755 index 0000000..d07278c --- /dev/null +++ b/.devcontainer/install-vault.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Install HashiCorp Vault +set -e + +echo "Installing HashiCorp Vault..." + +# Add HashiCorp GPG key +curl -fsSL https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null + +# Add HashiCorp repository +echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list + +# Update package index +sudo apt update + +# Install Vault +sudo apt install -y vault + +# Verify installation +vault version + +echo "Vault installation completed successfully!" \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..06a5de2 --- /dev/null +++ b/Makefile @@ -0,0 +1,197 @@ +.PHONY: help build install test clean fmt lint run check all + +# Default target +help: + @echo "Available targets:" + @echo "" + @echo "Build & Test:" + @echo " make build - Build the project in debug mode" + @echo " make release - Build the project in release mode" + @echo " make install - Install the binary locally" + @echo " make test - Run all tests" + @echo " make check - Run cargo check" + @echo " make fmt - Format code with rustfmt" + @echo " make lint - Run clippy linter" + @echo " make clean - Clean build artifacts" + @echo " make all - Format, lint, test, and build" + @echo "" + @echo "Running:" + @echo " make run - Run the application" + @echo " make init-config - Initialize a sample config file" + @echo " make run-example - Run with example config (needs Vault)" + @echo " make demo - Quick demo with Vault env vars" + @echo "" + @echo "Vault Development:" + @echo " make vault-docker - Start Vault in Docker (token: root)" + @echo " make vault-docker-stop - Stop Vault Docker container" + @echo " make vault-create-test-secrets - Create test secrets in Vault" + @echo " make vault-flag-test-secrets - Flag test secrets for rotation" + @echo " make vault-full-setup - Complete Vault setup with test data" + @echo " make dev-with-vault - Run scan with temporary Vault" + @echo " make install-vault - Install Vault CLI" + @echo " make vault-dev - Start Vault dev server (CLI)" + @echo " make vault-setup - Show Vault environment setup commands" + +# Build in debug mode +build: + cargo build + +# Build in release mode +release: + cargo build --release + +# Install the binary locally +install: + cargo install --path . + +# Run all tests +test: + cargo test + +# Run cargo check +check: + cargo check + +# Format code +fmt: + cargo fmt + +# Run clippy +lint: + cargo clippy -- -D warnings + +# Clean build artifacts +clean: + cargo clean + +# Run the application +run: + cargo run + +# Run in development mode with arguments +run-scan: + cargo run -- scan + +run-auto: + cargo run -- auto + +# Complete workflow: format, lint, test, and build +all: fmt lint test build + +# Watch mode for development (requires cargo-watch) +watch: + @command -v cargo-watch >/dev/null 2>&1 || { echo "cargo-watch not installed. Run: cargo install cargo-watch"; exit 1; } + cargo watch -x check -x test -x run + +# Install development dependencies +dev-deps: + cargo install cargo-watch + rustup component add rustfmt clippy + +# Generate documentation +docs: + cargo doc --no-deps --open + +# Run with example config +run-example: + @echo "Note: This requires Vault to be running. Use 'make vault-docker' in another terminal first." + cargo run -- --config examples/config.toml scan + +# Quick demo: run example with Vault environment variables (if Vault is running) +demo: + @echo "Running demo (ensure Vault is running with 'make vault-docker')..." + VAULT_ADDR='http://127.0.0.1:8200' VAULT_TOKEN='root' cargo run -- scan + +# Initialize a config file +init-config: + cargo run -- init + +# Install Vault CLI if not present +install-vault: + @if command -v vault >/dev/null 2>&1; then \ + echo "Vault CLI already installed: $$(vault version)"; \ + else \ + echo "Installing Vault CLI..."; \ + wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg >/dev/null; \ + echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $$(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list; \ + sudo apt update && sudo apt install -y vault; \ + echo "Vault CLI installed: $$(vault version)"; \ + fi + +# Development: start a local Vault server using Docker +vault-docker: + @if ! command -v docker >/dev/null 2>&1; then \ + echo "Docker not installed. Please install Docker first."; \ + exit 1; \ + fi + @echo "Starting Vault in Docker..." + @echo "Root token: root" + @echo "Vault address: http://127.0.0.1:8200" + docker run --rm --name vault-dev \ + -p 8200:8200 \ + -e 'VAULT_DEV_ROOT_TOKEN_ID=root' \ + -e 'VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200' \ + hashicorp/vault:latest + +# Stop the Docker Vault container +vault-docker-stop: + @docker stop vault-dev 2>/dev/null || echo "Vault container not running" + +# Development: start a local Vault server using Vault CLI +vault-dev: install-vault + vault server -dev -dev-root-token-id=root + +# Development: setup Vault dev environment in another terminal +vault-setup: + @echo "Run these commands in your terminal:" + @echo "export VAULT_ADDR='http://127.0.0.1:8200'" + @echo "export VAULT_TOKEN='root'" + +# Create test secrets in Vault (requires Vault to be running) +vault-create-test-secrets: + @echo "Creating test secrets in Vault..." + @docker exec vault-dev vault kv put secret/database/postgres password=old_postgres_pass username=dbuser || \ + VAULT_ADDR='https://127.0.0.1:8200' VAULT_TOKEN='root' vault kv put secret/database/postgres password=old_postgres_pass username=dbuser + @docker exec vault-dev vault kv put secret/api/github token=ghp_old_token_12345 || \ + VAULT_ADDR='https://127.0.0.1:8200' VAULT_TOKEN='root' vault kv put secret/api/github token=ghp_old_token_12345 + @docker exec vault-dev vault kv put secret/app/secret_key key=old_secret_key_value || \ + VAULT_ADDR='https://127.0.0.1:8200' VAULT_TOKEN='root' vault kv put secret/app/secret_key key=old_secret_key_value + @echo "Test secrets created!" + +# Flag test secrets for rotation +vault-flag-test-secrets: + @echo "Flagging test secrets for rotation..." + VAULT_ADDR='http://127.0.0.1:8200' VAULT_TOKEN='root' cargo run -- flag secret/database/postgres + VAULT_ADDR='httpmak://127.0.0.1:8200' VAULT_TOKEN='root' cargo run -- flag secret/api/github + @echo "Test secrets flagged!" + +# Complete setup: start Vault, create secrets, flag them +vault-full-setup: + @echo "Starting Vault in background..." + @docker run -d --rm --name vault-dev \ + -p 8200:8200 \ + -e 'VAULT_DEV_ROOT_TOKEN_ID=root' \ + -e 'VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200' \ + hashicorp/vault:latest >/dev/null 2>&1 || echo "Vault already running" + @echo "Waiting for Vault to be ready..." + @sleep 3 + @$(MAKE) vault-create-test-secrets + @$(MAKE) vault-flag-test-secrets + @echo "" + @echo "✓ Vault is ready with test secrets!" + @echo " Run: make demo" + +# Development: run with Vault Docker container (starts vault, runs command, stops vault) +dev-with-vault: + @echo "Starting Vault in background..." + @docker run -d --rm --name vault-dev-tmp \ + -p 8200:8200 \ + -e 'VAULT_DEV_ROOT_TOKEN_ID=root' \ + -e 'VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200' \ + hashicorp/vault:latest >/dev/null + @echo "Waiting for Vault to be ready..." + @sleep 3 + @echo "Running command with Vault..." + @VAULT_ADDR='http://127.0.0.1:8200' VAULT_TOKEN='root' cargo run -- scan || true + @echo "Stopping Vault..." + @docker stop vault-dev-tmp >/dev/null diff --git a/examples/config.toml b/examples/config.toml index 914d88d..e2896f2 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -6,7 +6,8 @@ address = "http://127.0.0.1:8200" # Vault token with permissions to read/write secrets and metadata # In production, use environment variables or secure secret management -token = "hvs.your-vault-token-here" +# For local development with 'make vault-docker', use: "root" +token = "root" # KV v2 mount point in Vault mount = "secret" diff --git a/examples/rotator-config.toml b/examples/rotator-config.toml new file mode 100644 index 0000000..86e0d93 --- /dev/null +++ b/examples/rotator-config.toml @@ -0,0 +1,8 @@ +[vault] +address = "http://127.0.0.1:8200" +token = "root" +mount = "secret" + +[rotation] +period_months = 6 +secret_length = 32 diff --git a/src/env_updater.rs b/src/env_updater.rs new file mode 100644 index 0000000..0870604 --- /dev/null +++ b/src/env_updater.rs @@ -0,0 +1,240 @@ +use anyhow::{Context, Result}; +use std::fs; +use std::path::{Path, PathBuf}; +use tracing::{debug, info, warn}; + +/// Updates environment variables in shell configuration files +#[allow(dead_code)] +pub struct EnvUpdater { + /// Home directory of the user + home_dir: PathBuf, +} + +impl EnvUpdater { + /// Create a new EnvUpdater for the current user + pub fn new() -> Result { + let home_dir = std::env::var("HOME") + .context("HOME environment variable not set")? + .into(); + + Ok(Self { home_dir }) + } + + /// Create an EnvUpdater for a specific home directory + pub fn with_home_dir(home_dir: PathBuf) -> Self { + Self { home_dir } + } + + /// Update or add an environment variable in shell config files + pub fn update_env_var(&self, var_name: &str, new_value: &str) -> Result<()> { + info!("Updating environment variable: {}", var_name); + + // Common shell config files + let config_files = vec![ + ".bashrc", + ".bash_profile", + ".zshrc", + ".profile", + ]; + + let mut updated_count = 0; + + for config_file in config_files { + let config_path = self.home_dir.join(config_file); + + if config_path.exists() { + match self.update_in_file(&config_path, var_name, new_value) { + Ok(true) => { + info!("Updated {} in {}", var_name, config_file); + updated_count += 1; + } + Ok(false) => { + debug!("{} not found in {}, appending", var_name, config_file); + self.append_to_file(&config_path, var_name, new_value)?; + updated_count += 1; + } + Err(e) => { + warn!("Failed to update {}: {}", config_file, e); + } + } + } + } + + if updated_count == 0 { + warn!("No shell config files found or updated"); + } + + Ok(()) + } + + /// Update environment variable in a specific file + fn update_in_file(&self, path: &Path, var_name: &str, new_value: &str) -> Result { + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + + let export_pattern = format!("export {}=", var_name); + let mut found = false; + let mut new_content = String::new(); + + for line in content.lines() { + let trimmed = line.trim(); + + // Check if this line exports our variable + if trimmed.starts_with(&export_pattern) || + trimmed.starts_with(&format!("{}=", var_name)) { + // Replace the line with the new value + new_content.push_str(&format!("export {}=\"{}\"\n", var_name, new_value)); + found = true; + } else { + new_content.push_str(line); + new_content.push('\n'); + } + } + + if found { + fs::write(path, new_content) + .with_context(|| format!("Failed to write to {}", path.display()))?; + } + + Ok(found) + } + + /// Append environment variable to a file + fn append_to_file(&self, path: &Path, var_name: &str, new_value: &str) -> Result<()> { + let mut content = fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + + // Add a newline if the file doesn't end with one + if !content.ends_with('\n') { + content.push('\n'); + } + + // Add a comment and the new export + content.push_str(&format!( + "\n# Auto-updated by secret rotator\nexport {}=\"{}\"\n", + var_name, new_value + )); + + fs::write(path, content) + .with_context(|| format!("Failed to write to {}", path.display()))?; + + Ok(()) + } + + /// Remove an environment variable from shell config files + pub fn remove_env_var(&self, var_name: &str) -> Result<()> { + info!("Removing environment variable: {}", var_name); + + let config_files = vec![ + ".bashrc", + ".bash_profile", + ".zshrc", + ".profile", + ]; + + for config_file in config_files { + let config_path = self.home_dir.join(config_file); + + if config_path.exists() { + self.remove_from_file(&config_path, var_name)?; + } + } + + Ok(()) + } + + /// Remove environment variable from a specific file + fn remove_from_file(&self, path: &Path, var_name: &str) -> Result<()> { + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + + let export_pattern = format!("export {}=", var_name); + let mut new_content = String::new(); + let mut skip_next_comment = false; + + for line in content.lines() { + let trimmed = line.trim(); + + // Skip the auto-update comment if we're about to remove a variable + if trimmed == "# Auto-updated by secret rotator" { + skip_next_comment = true; + continue; + } + + // Check if this line exports our variable + if trimmed.starts_with(&export_pattern) || + trimmed.starts_with(&format!("{}=", var_name)) { + skip_next_comment = false; + continue; // Skip this line + } + + if skip_next_comment { + skip_next_comment = false; + new_content.push_str(line); + new_content.push('\n'); + } else { + new_content.push_str(line); + new_content.push('\n'); + } + } + + fs::write(path, new_content) + .with_context(|| format!("Failed to write to {}", path.display()))?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + #[test] + fn test_update_new_variable() -> Result<()> { + let temp_dir = TempDir::new()?; + let bashrc = temp_dir.path().join(".bashrc"); + fs::write(&bashrc, "# existing config\n")?; + + let updater = EnvUpdater::with_home_dir(temp_dir.path().to_path_buf()); + updater.update_env_var("MY_SECRET", "new_value")?; + + let content = fs::read_to_string(&bashrc)?; + assert!(content.contains("export MY_SECRET=\"new_value\"")); + + Ok(()) + } + + #[test] + fn test_update_existing_variable() -> Result<()> { + let temp_dir = TempDir::new()?; + let bashrc = temp_dir.path().join(".bashrc"); + fs::write(&bashrc, "export MY_SECRET=\"old_value\"\n")?; + + let updater = EnvUpdater::with_home_dir(temp_dir.path().to_path_buf()); + updater.update_env_var("MY_SECRET", "new_value")?; + + let content = fs::read_to_string(&bashrc)?; + assert!(content.contains("export MY_SECRET=\"new_value\"")); + assert!(!content.contains("old_value")); + + Ok(()) + } + + #[test] + fn test_remove_variable() -> Result<()> { + let temp_dir = TempDir::new()?; + let bashrc = temp_dir.path().join(".bashrc"); + fs::write(&bashrc, "export MY_SECRET=\"value\"\n# other config\n")?; + + let updater = EnvUpdater::with_home_dir(temp_dir.path().to_path_buf()); + updater.remove_env_var("MY_SECRET")?; + + let content = fs::read_to_string(&bashrc)?; + assert!(!content.contains("MY_SECRET")); + assert!(content.contains("# other config")); + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 1dbf3f1..00a5645 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod config; +mod env_updater; mod rotation; mod vault; @@ -90,6 +91,38 @@ enum Commands { #[arg(default_value = "")] path: String, }, + + /// Update a local environment variable with a secret from Vault + UpdateEnv { + /// Path to the secret in Vault + vault_path: String, + + /// Key within the secret data + #[arg(short, long, default_value = "password")] + key: String, + + /// Environment variable name to update + #[arg(short, long)] + env_var: String, + }, + + /// Generate a new password, store it in Vault, and update local environment variable + GenPassword { + /// Path to store the secret in Vault + vault_path: String, + + /// Key name for the password in Vault + #[arg(short, long, default_value = "password")] + key: String, + + /// Environment variable name to update (optional) + #[arg(short, long)] + env_var: Option, + + /// Length of the generated password + #[arg(short, long)] + length: Option, + }, } #[tokio::main] @@ -256,6 +289,77 @@ async fn main() -> Result<()> { } } } + + Commands::UpdateEnv { + vault_path, + key, + env_var, + } => { + // Read the secret from Vault + let secret = vault + .read_secret(&config.vault.mount, &vault_path) + .await + .context("Failed to read secret from Vault")?; + + // Get the specific key value + let value = secret + .data + .get(&key) + .with_context(|| format!("Key '{}' not found in secret", key))?; + + // Update the environment variable + let env_updater = env_updater::EnvUpdater::new() + .context("Failed to create EnvUpdater")?; + + env_updater + .update_env_var(&env_var, value) + .with_context(|| format!("Failed to update environment variable {}", env_var))?; + + println!("✓ Updated environment variable '{}' in shell config files", env_var); + println!(" Value synced from Vault: {}/{} (key: {})", config.vault.mount, vault_path, key); + println!("\n⚠️ Note: You need to reload your shell or run 'source ~/.bashrc' (or ~/.zshrc) for changes to take effect"); + } + + Commands::GenPassword { + vault_path, + key, + env_var, + length, + } => { + // Generate a new password + let password_length = length.unwrap_or(config.rotation.secret_length); + let new_password = rotation::generate_secret(password_length); + + // Prepare secret data + let mut secret_data = std::collections::HashMap::new(); + secret_data.insert(key.clone(), new_password.clone()); + + // Store in Vault + vault + .write_secret(&config.vault.mount, &vault_path, secret_data) + .await + .context("Failed to write secret to Vault")?; + + println!("✓ Generated new password and stored in Vault"); + println!(" Location: {}/{}", config.vault.mount, vault_path); + println!(" Key: {}", key); + println!(" Length: {} characters", password_length); + + // Update local environment variable if specified + if let Some(env_var_name) = env_var { + let env_updater = env_updater::EnvUpdater::new() + .context("Failed to create EnvUpdater")?; + + env_updater + .update_env_var(&env_var_name, &new_password) + .with_context(|| format!("Failed to update environment variable {}", env_var_name))?; + + println!("✓ Updated environment variable '{}' in shell config files", env_var_name); + println!("\n⚠️ Note: You need to reload your shell or run 'source ~/.bashrc' (or ~/.zshrc) for changes to take effect"); + } else { + println!("\n💡 Tip: Use --env-var to automatically update a local environment variable"); + } + } } Ok(()) diff --git a/src/vault.rs b/src/vault.rs index 27d066a..fe1b9c5 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -186,6 +186,12 @@ impl VaultClient { .await .context("Failed to list secrets from Vault")?; + // 404 means no secrets exist at this path, which is fine + if response.status() == 404 { + info!("No secrets found at {}/{} (empty path)", mount, path); + return Ok(vec![]); + } + if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_default(); From da2d5807bab09009fff2df460d12bfb0705b918a Mon Sep 17 00:00:00 2001 From: kelleyblackmore Date: Wed, 12 Nov 2025 08:48:24 +0000 Subject: [PATCH 2/5] updates --- README.md | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++- USAGE.md | 95 +++++++++++++++++++++++++- install.sh | 66 ++++++++++++++++++ src/main.rs | 35 +++++++++- 4 files changed, 380 insertions(+), 6 deletions(-) create mode 100755 install.sh diff --git a/README.md b/README.md index b442045..8d831fe 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,72 @@ A Rust-based CLI tool for automatic secret rotation with HashiCorp Vault integra - 🔍 **Scanning**: Scan vault paths to identify secrets needing rotation - 📝 **Metadata Tracking**: Uses Vault metadata to track rotation status and schedules - 🎯 **Flexible Configuration**: Configure via file or environment variables +- 🔑 **Password Generation**: Generate secure random passwords and store them in Vault +- 💻 **Environment Variable Sync**: Automatically update local shell config files with rotated secrets +- ⚡ **Auto-Update Workflow**: Rotate secrets and update environment variables in one command ## Installation +### Prerequisites + +- Rust 1.70+ (will be auto-installed by the installer script) +- HashiCorp Vault server (or use Docker for local development) + +### Quick Install (Recommended) + +Install with a single command: + +```bash +curl -fsSL https://raw.githubusercontent.com/kelleyblackmore/Automatic-Secret-Rotation/main/install.sh | bash +``` + +This will: +1. Install Rust (if not already installed) +2. Clone the repository +3. Build and install `asr` to `~/.local/bin/asr` +4. Verify the installation + +After installation, you may need to add `~/.local/bin` to your PATH: + +```bash +echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc +source ~/.bashrc +``` + ### From Source ```bash -cargo install --path . +git clone https://github.com/kelleyblackmore/Automatic-Secret-Rotation.git +cd Automatic-Secret-Rotation +make install ``` ### Build Binary ```bash -cargo build --release +make build # Debug build +make release # Release build # Binary will be in target/release/asr ``` +### Using Cargo + +```bash +cargo install --git https://github.com/kelleyblackmore/Automatic-Secret-Rotation +``` + +### Local Development + +The project includes a Makefile with helpful commands: + +```bash +make help # Show all available commands +make vault-docker # Start Vault in Docker for testing +make vault-full-setup # Start Vault with test secrets +make test # Run tests +make all # Format, lint, test, and build +``` + ## Quick Start ### 1. Initialize Configuration @@ -74,12 +124,56 @@ Rotate all secrets that are due: asr auto ``` -Or perform a dry-run first: +Or with automatic environment variable updates: + +```bash +asr auto --update-env +``` + +Perform a dry-run first: ```bash asr auto --dry-run ``` +## Password Management + +### Generate New Password + +Generate a secure password, store it in Vault, and optionally update your local environment: + +```bash +# Generate and store in Vault only +asr gen-password myapp/database + +# Generate, store, AND update local environment variable +asr gen-password --env-var DB_PASSWORD myapp/database + +# Custom length +asr gen-password --env-var API_KEY --length 48 myapp/api + +# Custom key name in Vault (default is "password") +asr gen-password --env-var TOKEN --key token myapp/github +``` + +### Sync Vault Secret to Environment Variable + +Update your local shell config with an existing Vault secret: + +```bash +# Sync secret to environment variable +asr update-env --env-var DB_PASSWORD myapp/database + +# Sync with custom key +asr update-env --env-var API_TOKEN --key token myapp/github +``` + +After updating environment variables: +```bash +source ~/.bashrc # or ~/.zshrc +echo $DB_PASSWORD # Verify it's set +``` + ## Usage ### Configuration @@ -175,6 +269,49 @@ asr auto --dry-run # Scan specific path asr auto app/ + +# Rotate and update environment variables +asr auto --update-env +``` + +When using `--update-env`, environment variables are automatically created based on the secret path: +- `myapp/database` → `MYAPP_DATABASE` +- `api/github` → `API_GITHUB` + +#### `gen-password` - Generate New Password + +Generate a secure random password and store it in Vault: + +```bash +# Generate and store in Vault +asr gen-password myapp/database + +# Generate and update local environment variable +asr gen-password --env-var DB_PASSWORD myapp/database + +# Custom length (default: 32 characters) +asr gen-password --length 48 myapp/api-key + +# Custom key name (default: "password") +asr gen-password --key token --env-var API_TOKEN myapp/github +``` + +#### `update-env` - Sync Vault Secret to Environment + +Update local environment variables with secrets from Vault: + +```bash +# Update environment variable from Vault +asr update-env --env-var DB_PASSWORD myapp/database + +# Use custom key from secret +asr update-env --env-var API_TOKEN --key token myapp/github +``` + +This command updates your shell configuration files (`.bashrc`, `.bash_profile`, `.zshrc`, `.profile`) with the secret value. You'll need to reload your shell for changes to take effect: + +```bash +source ~/.bashrc ``` #### `read` - Read a Secret @@ -291,6 +428,7 @@ The tool uses Vault's custom metadata feature to track rotation status: 2. **Scanning**: The tool reads metadata to identify secrets needing rotation 3. **Rotation**: New random secrets are generated and written to Vault 4. **Tracking**: Metadata is updated with the new rotation timestamp +5. **Environment Sync** (optional): Local shell configs are updated with new values ### Secret Generation @@ -300,6 +438,17 @@ Secrets are generated using cryptographically secure random number generation wi - Numbers (0-9) - Special characters (!@#$%^&*) +### Environment Variable Management + +The tool can automatically update your shell configuration files with rotated secrets: + +1. **Shell Config Files**: Updates `.bashrc`, `.bash_profile`, `.zshrc`, and `.profile` +2. **Smart Updates**: If a variable already exists, it's updated in-place; otherwise, it's appended +3. **Comments**: Adds `# Auto-updated by secret rotator` for tracking +4. **Path Mapping**: Converts Vault paths to environment variable names (e.g., `myapp/database` → `MYAPP_DATABASE`) + +This enables seamless integration of Vault-managed secrets with applications that read from environment variables. + ## Security Considerations - **Vault Token**: Store Vault tokens securely using secret management in your CI/CD platform @@ -386,6 +535,41 @@ cargo run -- --help MIT License - see LICENSE file for details +## Quick Reference + +### Common Commands + +```bash +# Generate password with env var update +asr gen-password --env-var DB_PASS myapp/db + +# Sync Vault secret to environment +asr update-env --env-var API_KEY myapp/api + +# Rotate all due secrets and update env vars +asr auto --update-env + +# Flag secret for rotation +asr flag myapp/password --period 3 + +# Scan for secrets needing rotation +asr scan + +# Dry run auto-rotation +asr auto --dry-run +``` + +### Makefile Commands + +```bash +make install # Install the binary +make vault-docker # Start Vault in Docker +make vault-full-setup # Start Vault with test data +make demo # Quick demo with Vault +make test # Run tests +make all # Format, lint, test, build +``` + ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/USAGE.md b/USAGE.md index 08186b4..26aa685 100644 --- a/USAGE.md +++ b/USAGE.md @@ -14,7 +14,13 @@ This guide provides step-by-step instructions for using the asr tool. ### 1. Install the Tool -Build from source: +**Quick install (recommended):** + +```bash +curl -fsSL https://raw.githubusercontent.com/kelleyblackmore/Automatic-Secret-Rotation/main/install.sh | bash +``` + +**Or build from source:** ```bash git clone https://github.com/kelleyblackmore/Automatic-Secret-Rotation.git @@ -23,6 +29,14 @@ cargo build --release sudo cp target/release/asr /usr/local/bin/ ``` +**Or use the Makefile:** + +```bash +git clone https://github.com/kelleyblackmore/Automatic-Secret-Rotation.git +cd Automatic-Secret-Rotation +make install +``` + ### 2. Set Up HashiCorp Vault If you don't have Vault set up yet: @@ -98,6 +112,10 @@ Secrets needing rotation: #### Manual Rotation +### Step 3: Rotate Secrets + +#### Manual Rotation + Rotate a specific secret: ```bash @@ -120,6 +138,41 @@ asr auto --dry-run # Perform rotation asr auto + +# Rotate and update environment variables +asr auto --update-env +``` + +### Step 4: Password Generation & Environment Management + +#### Generate New Password + +Create a new password and store it in Vault: + +```bash +# Just generate and store +asr gen-password myapp/database + +# Generate and update environment variable +asr gen-password --env-var DB_PASSWORD myapp/database + +# Custom length +asr gen-password --env-var API_KEY --length 48 myapp/api +``` + +#### Sync Vault to Environment + +Update your local environment with secrets from Vault: + +```bash +# Update environment variable +asr update-env --env-var DB_PASSWORD myapp/database + +# Custom key name +asr update-env --env-var TOKEN --key token myapp/github + +# Reload shell to apply changes +source ~/.bashrc ``` ## Advanced Usage @@ -360,6 +413,46 @@ asr auto --dry-run asr auto ``` +### Scenario 7: Environment Variable Automation + +Automatically sync secrets to your development environment: + +```bash +# Generate a new database password and update env var +asr gen-password --env-var DB_PASSWORD myapp/database + +# Rotate secrets and update all environment variables +asr auto --update-env + +# Reload shell +source ~/.bashrc + +# Your app can now use the env vars +echo $DB_PASSWORD +echo $MYAPP_DATABASE # Auto-named from path +``` + +### Scenario 8: Password Management Workflow + +Complete workflow for managing passwords with environment integration: + +```bash +# 1. Generate initial password +asr gen-password --env-var APP_SECRET --length 32 myapp/secret + +# 2. Flag for rotation every 3 months +asr flag myapp/secret --period 3 + +# 3. Later, when it needs rotation (or manually) +asr rotate myapp/secret + +# 4. Update environment variable with new value +asr update-env --env-var APP_SECRET myapp/secret + +# 5. Or do both in one step during auto-rotation +asr auto --update-env +``` + ## Troubleshooting ### Issue: "Failed to connect to Vault" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..51be4f9 --- /dev/null +++ b/install.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -e + +# Automatic Secret Rotation (asr) installer +# Usage: curl -fsSL https://raw.githubusercontent.com/kelleyblackmore/Automatic-Secret-Rotation/main/install.sh | bash + +echo "Installing Automatic Secret Rotation (asr)..." + +# Check if Rust is installed +if ! command -v cargo &> /dev/null; then + echo "Rust/Cargo not found. Installing Rust..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source "$HOME/.cargo/env" +fi + +# Check Rust version +RUST_VERSION=$(rustc --version | awk '{print $2}') +echo "Using Rust version: $RUST_VERSION" + +# Clone or update repository +INSTALL_DIR="${ASR_INSTALL_DIR:-$HOME/.asr}" +if [ -d "$INSTALL_DIR" ]; then + echo "Updating existing installation at $INSTALL_DIR..." + cd "$INSTALL_DIR" + git pull +else + echo "Cloning repository to $INSTALL_DIR..." + git clone https://github.com/kelleyblackmore/Automatic-Secret-Rotation.git "$INSTALL_DIR" + cd "$INSTALL_DIR" +fi + +# Build and install +echo "Building asr..." +cargo build --release + +# Install to user bin +BIN_DIR="${HOME}/.local/bin" +mkdir -p "$BIN_DIR" +cp target/release/asr "$BIN_DIR/" + +# Check if ~/.local/bin is in PATH +if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then + echo "" + echo "⚠️ Add $BIN_DIR to your PATH by adding this to your ~/.bashrc or ~/.zshrc:" + echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" + echo "" +fi + +# Verify installation +if command -v asr &> /dev/null; then + echo "" + echo "✅ asr installed successfully!" + asr --version + echo "" + echo "Get started with:" + echo " asr --help" + echo " asr init # Create a config file" +else + echo "" + echo "⚠️ Installation complete, but 'asr' is not in PATH." + echo "Add $BIN_DIR to your PATH or run directly: $BIN_DIR/asr" +fi + +echo "" +echo "For more information, visit:" +echo " https://github.com/kelleyblackmore/Automatic-Secret-Rotation" diff --git a/src/main.rs b/src/main.rs index 00a5645..3db89cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,6 +77,10 @@ enum Commands { /// Dry run - only show what would be rotated #[arg(long)] dry_run: bool, + + /// Also update local environment variables (expects env var name to match secret path) + #[arg(long)] + update_env: bool, }, /// Read a secret from Vault @@ -218,7 +222,7 @@ async fn main() -> Result<()> { eprintln!("⚠️ Please update your application with the new secret and clear your terminal history."); } - Commands::Auto { path, dry_run } => { + Commands::Auto { path, dry_run, update_env } => { let secrets = rotation::scan_for_rotation( &vault, &config.vault.mount, @@ -235,9 +239,18 @@ async fn main() -> Result<()> { println!("Found {} secret(s) needing rotation", secrets.len()); + let env_updater = if update_env { + Some(env_updater::EnvUpdater::new().context("Failed to create EnvUpdater")?) + } else { + None + }; + for secret_path in &secrets { if dry_run { println!("[DRY RUN] Would rotate: {}", secret_path); + if update_env { + println!(" [DRY RUN] Would update env var based on path"); + } } else { match rotation::rotate_secret( &vault, @@ -247,7 +260,22 @@ async fn main() -> Result<()> { ) .await { - Ok(_) => println!("✓ Rotated: {}", secret_path), + Ok(new_value) => { + println!("✓ Rotated: {}", secret_path); + + // Update environment variable if requested + if let Some(ref updater) = env_updater { + // Convert path to env var name: myapp/database -> MYAPP_DATABASE + let env_var_name = secret_path + .replace('/', "_") + .to_uppercase(); + + match updater.update_env_var(&env_var_name, &new_value) { + Ok(_) => println!(" ✓ Updated env var: {}", env_var_name), + Err(e) => eprintln!(" ✗ Failed to update env var {}: {}", env_var_name, e), + } + } + } Err(e) => { error!("✗ Failed to rotate {}: {}", secret_path, e); } @@ -257,6 +285,9 @@ async fn main() -> Result<()> { if !dry_run { println!("\nRotation complete!"); + if update_env { + println!("⚠️ Note: Reload your shell or run 'source ~/.bashrc' for env var changes to take effect"); + } } } From 9a1c5e3179702f68bd40aa99c04aaee86e1d2497 Mon Sep 17 00:00:00 2001 From: kelleyblackmore Date: Wed, 12 Nov 2025 01:51:26 -0700 Subject: [PATCH 3/5] Update src/env_updater.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/env_updater.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/env_updater.rs b/src/env_updater.rs index 0870604..becdf30 100644 --- a/src/env_updater.rs +++ b/src/env_updater.rs @@ -161,6 +161,12 @@ impl EnvUpdater { continue; } + // Skip commented out lines + if trimmed.starts_with('#') { + new_content.push_str(line); + new_content.push('\n'); + continue; + } // Check if this line exports our variable if trimmed.starts_with(&export_pattern) || trimmed.starts_with(&format!("{}=", var_name)) { From 69d882729385e37dfc615e9683375d3b98311676 Mon Sep 17 00:00:00 2001 From: kelleyblackmore Date: Wed, 12 Nov 2025 01:52:14 -0700 Subject: [PATCH 4/5] Update src/env_updater.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/env_updater.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/env_updater.rs b/src/env_updater.rs index becdf30..6afbfb8 100644 --- a/src/env_updater.rs +++ b/src/env_updater.rs @@ -174,14 +174,9 @@ impl EnvUpdater { continue; // Skip this line } - if skip_next_comment { - skip_next_comment = false; - new_content.push_str(line); - new_content.push('\n'); - } else { - new_content.push_str(line); - new_content.push('\n'); - } + skip_next_comment = false; + new_content.push_str(line); + new_content.push('\n'); } fs::write(path, new_content) From 7fb6460ed1015cc7cec215c1ecc285e6eb107a01 Mon Sep 17 00:00:00 2001 From: kelleyblackmore Date: Wed, 12 Nov 2025 01:52:41 -0700 Subject: [PATCH 5/5] Update Makefile Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 06a5de2..eb43a84 100644 --- a/Makefile +++ b/Makefile @@ -162,7 +162,7 @@ vault-create-test-secrets: vault-flag-test-secrets: @echo "Flagging test secrets for rotation..." VAULT_ADDR='http://127.0.0.1:8200' VAULT_TOKEN='root' cargo run -- flag secret/database/postgres - VAULT_ADDR='httpmak://127.0.0.1:8200' VAULT_TOKEN='root' cargo run -- flag secret/api/github + VAULT_ADDR='http://127.0.0.1:8200' VAULT_TOKEN='root' cargo run -- flag secret/api/github @echo "Test secrets flagged!" # Complete setup: start Vault, create secrets, flag them