diff --git a/.gitignore b/.gitignore index 0054e88..b7370a8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ Cargo.lock # Vault related files vault-credentials.txt +vault-credentials.json vault-config/ # Temporary test files diff --git a/src/main.rs b/src/main.rs index 2460556..0fca6e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ use std::{ fs::File, io::Write, path::Path, - process::Command, time::Duration, }; use tokio::time::sleep; @@ -40,6 +39,21 @@ struct UnsealRequest { // Function to save Vault credentials to a file fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> { + // For JSON output + if file_path.ends_with(".json") { + let json = serde_json::json!({ + "keys": response.keys, + "keys_base64": response.keys_base64, + "root_token": response.root_token + }); + + let mut file = File::create(Path::new(file_path))?; + file.write_all(serde_json::to_string_pretty(&json)?.as_bytes())?; + println!("Credentials saved to JSON file: {}", file_path); + return Ok(()); + } + + // For plaintext output (legacy format) let mut file = File::create(Path::new(file_path))?; writeln!(file, "Unseal Keys:")?; for (i, key) in response.keys.iter().enumerate() { @@ -51,7 +65,7 @@ fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> { } writeln!(file)?; writeln!(file, "Root Token: {}", response.root_token)?; - + println!("Credentials saved to {}", file_path); Ok(()) } @@ -59,12 +73,12 @@ fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> { // Wait for Vault to become available async fn wait_for_vault(addr: &str) -> Result<()> { println!("Waiting for Vault to be ready..."); - + let client = Client::new(); - + for i in 1..=30 { let health_url = format!("{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true", addr); - + match client.get(&health_url).timeout(Duration::from_secs(1)).send().await { Ok(response) => { let status = response.status().as_u16(); @@ -73,121 +87,55 @@ async fn wait_for_vault(addr: &str) -> Result<()> { println!("Vault is available! Status code: {}", status); return Ok(()); } - + println!("Vault returned unexpected status code: {}", status); }, Err(e) => { println!("Error connecting to Vault: {}", e); } } - + if i == 30 { return Err(anyhow::anyhow!("Timed out waiting for Vault to become available")); } - + println!("Vault is unavailable - sleeping (attempt {}/30)", i); sleep(Duration::from_secs(2)).await; } - - Ok(()) -} -// Function to copy credentials to a mounted volume if available -fn copy_credentials_to_volume(src_path: &str) -> Result<()> { - println!("Searching for credentials file..."); - - if let Ok(metadata) = std::fs::metadata(src_path) { - if metadata.is_file() { - println!("Found credentials at {}, copying...", src_path); - - // Create the data directory if it doesn't exist - if let Err(e) = std::fs::create_dir_all("/app/data") { - println!("Warning: Couldn't create /app/data directory: {}", e); - } else { - let dest_path = "/app/data/vault-credentials.txt"; - - // Check if source and destination are the same - if src_path == dest_path { - println!("Source and destination are the same file, skipping copy"); - } else { - match std::fs::copy(src_path, dest_path) { - Ok(_) => println!("Credentials saved to {}", dest_path), - Err(e) => println!("Failed to copy credentials: {}", e), - } - } - } - } - } else { - // If the file doesn't exist in the current directory, search for it - let output = Command::new("find") - .args(["/", "-name", "vault-credentials.txt", "-type", "f"]) - .output(); - - match output { - Ok(output) => { - let files = String::from_utf8_lossy(&output.stdout); - let files: Vec<&str> = files.split('\n').filter(|s| !s.is_empty()).collect(); - - if !files.is_empty() { - println!("Found credentials at {}, copying...", files[0]); - - // Create the data directory if it doesn't exist - if let Err(e) = std::fs::create_dir_all("/app/data") { - println!("Warning: Couldn't create /app/data directory: {}", e); - } else { - let dest_path = "/app/data/vault-credentials.txt"; - - // Check if source and destination are the same - if files[0] == dest_path { - println!("Source and destination are the same file, skipping copy"); - } else { - match std::fs::copy(files[0], dest_path) { - Ok(_) => println!("Credentials saved to {}", dest_path), - Err(e) => println!("Failed to copy credentials: {}", e), - } - } - } - } else { - println!("Could not find credentials file"); - } - }, - Err(e) => println!("Failed to search for credentials: {}", e), - } - } - Ok(()) } async fn check_init_status(client: &Client, addr: &str) -> Result { println!("Checking if Vault is already initialized..."); - + let response = client .get(format!("{}/v1/sys/init", addr)) .send() .await?; - + if response.status().is_success() { let status = response.json::().await?; if let Some(initialized) = status.get("initialized").and_then(|v| v.as_bool()) { return Ok(initialized); } } - + // If we couldn't determine, assume not initialized Ok(false) } async fn check_seal_status(client: &Client, addr: &str) -> Result { println!("Checking Vault seal status..."); - + let response = client .get(format!("{}/v1/sys/seal-status", addr)) .send() .await?; - + if response.status().is_success() { let status = response.json::().await?; - println!("Seal status: sealed={}, threshold={}, shares={}, progress={}", + println!("Seal status: sealed={}, threshold={}, shares={}, progress={}", status.sealed, status.t, status.n, status.progress); return Ok(status); } else { @@ -199,26 +147,26 @@ async fn check_seal_status(client: &Client, addr: &str) -> Result Result { // First check if already initialized let initialized = check_init_status(client, addr).await?; - + if initialized { anyhow::bail!("Vault is already initialized. Cannot re-initialize."); } - + println!("Initializing Vault..."); - + // Configure with 5 key shares and a threshold of 3 // This is a standard production configuration, requiring 3 out of 5 keys to unseal let init_req = InitRequest { secret_shares: 5, secret_threshold: 3, }; - + let response = client .put(format!("{}/v1/sys/init", addr)) .json(&init_req) .send() .await?; - + match response.status() { StatusCode::OK => { let init_response = response.json::().await?; @@ -235,59 +183,59 @@ async fn init_vault(client: &Client, addr: &str) -> Result { async fn unseal_vault(client: &Client, addr: &str, unseal_keys: &[String]) -> Result<()> { // First check the current seal status let mut seal_status = check_seal_status(client, addr).await?; - + if !seal_status.sealed { println!("Vault is already unsealed!"); return Ok(()); } - + println!("Unsealing Vault..."); - + // We need to provide enough keys to meet the threshold // The threshold is in seal_status.t let required_keys = seal_status.t as usize; - + if unseal_keys.len() < required_keys { anyhow::bail!( "Not enough unseal keys provided. Need {} keys, but only have {}", - required_keys, + required_keys, unseal_keys.len() ); } - + // Apply each key one at a time until unsealed for (i, key) in unseal_keys.iter().take(required_keys).enumerate() { println!("Applying unseal key {}/{}...", i + 1, required_keys); - + let unseal_req = UnsealRequest { key: key.clone(), }; - + let response = client .put(format!("{}/v1/sys/unseal", addr)) .json(&unseal_req) .send() .await?; - + if !response.status().is_success() { let error_text = response.text().await?; anyhow::bail!("Failed to apply unseal key: {}", error_text); } - + // Check the updated seal status seal_status = check_seal_status(client, addr).await?; - + if !seal_status.sealed { println!("Vault unsealed successfully after applying {} keys!", i + 1); return Ok(()); } } - + // If we get here, we've applied all keys but Vault is still sealed if seal_status.sealed { anyhow::bail!("Applied all available unseal keys, but Vault is still sealed"); } - + Ok(()) } @@ -296,13 +244,13 @@ async fn main() -> Result<()> { // Get Vault address from env var or use default let vault_addr = env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()); let client = Client::new(); - + println!("Vault address: {}", vault_addr); println!("Connecting to Vault at: {}", vault_addr); - + // Wait for Vault to be available wait_for_vault(&vault_addr).await?; - + // Get Vault status to display let health_url = format!("{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true", vault_addr); match client.get(&health_url).send().await { @@ -314,19 +262,19 @@ async fn main() -> Result<()> { }, Err(e) => println!("Error getting Vault status: {}", e), } - + // First check if Vault is already initialized let initialized = check_init_status(&client, &vault_addr).await?; - + if initialized { println!("Vault is already initialized."); - + // Check if Vault is sealed let seal_status = check_seal_status(&client, &vault_addr).await?; - + if seal_status.sealed { println!("Vault is sealed. Looking for unseal keys..."); - + // Try to load unseal keys from environment variables let mut unseal_keys = Vec::new(); for i in 1..=5 { @@ -340,7 +288,7 @@ async fn main() -> Result<()> { } } } - + // If we have unseal keys, try to unseal if !unseal_keys.is_empty() { println!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len()); @@ -356,21 +304,32 @@ async fn main() -> Result<()> { // Initialize Vault println!("Vault is not initialized. Proceeding with initialization..."); let init_response = init_vault(&client, &vault_addr).await?; - + // Save credentials to files - println!("Saving credentials to file..."); + println!("Saving credentials to files..."); let current_dir = std::env::current_dir().context("Failed to get current directory")?; - let cred_path = current_dir.join("vault-credentials.txt"); - save_credentials(&init_response, cred_path.to_str().unwrap())?; - println!("Credentials saved to: {}", cred_path.display()); + // Save as JSON (new format) + let json_path = current_dir.join("vault-credentials.json"); + save_credentials(&init_response, json_path.to_str().unwrap())?; + println!("JSON credentials saved to: {}", json_path.display()); + + // Save as text (for backward compatibility) + let text_path = current_dir.join("vault-credentials.txt"); + save_credentials(&init_response, text_path.to_str().unwrap())?; + println!("Text credentials saved to: {}", text_path.display()); + // Also save to /app/data as a backup for Docker volume mounting if let Ok(()) = std::fs::create_dir_all("/app/data") { - let docker_path = "/app/data/vault-credentials.txt"; - save_credentials(&init_response, docker_path)?; - println!("Backup credentials saved to Docker volume at: {}", docker_path); + let docker_json_path = "/app/data/vault-credentials.json"; + save_credentials(&init_response, docker_json_path)?; + println!("Backup JSON credentials saved to Docker volume at: {}", docker_json_path); + + let docker_text_path = "/app/data/vault-credentials.txt"; + save_credentials(&init_response, docker_text_path)?; + println!("Backup text credentials saved to Docker volume at: {}", docker_text_path); } - + println!("========================================="); println!("IMPORTANT: SAVE THESE CREDENTIALS SECURELY"); println!("========================================="); @@ -380,33 +339,44 @@ async fn main() -> Result<()> { println!("Key {}: {}", i + 1, key); } println!("========================================="); - + // Unseal Vault using the first three keys let unseal_keys = init_response.keys_base64.iter() .take(3) // We only need threshold number of keys (3) .cloned() .collect::>(); - + unseal_vault(&client, &vault_addr, &unseal_keys).await?; - - println!("Vault is now initialized and unsealed"); - - // Store the root token and unseal keys in environment variables - // Using unsafe block as set_var is now considered unsafe in recent Rust - unsafe { - env::set_var("VAULT_TOKEN", &init_response.root_token); - for (i, key) in init_response.keys_base64.iter().enumerate() { - env::set_var(format!("VAULT_UNSEAL_KEY_{}", i + 1), key); - } - } - + println!("Vault initialization and unseal complete!"); } - // Copy credentials to the mounted volume (former docker-entrypoint.sh functionality) - copy_credentials_to_volume("vault-credentials.txt")?; + // Look for any existing credentials and copy them to the mounted volume + if let Ok(metadata) = std::fs::metadata("vault-credentials.json") { + if metadata.is_file() { + println!("Found JSON credentials file, ensuring it's saved to Docker volume..."); + if let Ok(()) = std::fs::create_dir_all("/app/data") { + match std::fs::copy("vault-credentials.json", "/app/data/vault-credentials.json") { + Ok(_) => println!("JSON credentials saved to Docker volume"), + Err(e) => println!("Failed to copy JSON credentials: {}", e), + } + } + } + } + if let Ok(metadata) = std::fs::metadata("vault-credentials.txt") { + if metadata.is_file() { + println!("Found text credentials file, ensuring it's saved to Docker volume..."); + if let Ok(()) = std::fs::create_dir_all("/app/data") { + match std::fs::copy("vault-credentials.txt", "/app/data/vault-credentials.txt") { + Ok(_) => println!("Text credentials saved to Docker volume"), + Err(e) => println!("Failed to copy text credentials: {}", e), + } + } + } + } + println!("Operation complete!"); - + Ok(()) -} \ No newline at end of file +} diff --git a/test_docker.sh b/test_docker.sh index 12f4b32..b608760 100755 --- a/test_docker.sh +++ b/test_docker.sh @@ -99,22 +99,28 @@ wait_for_vault_init() { # Wait for vault-init to complete wait_for_vault_init -# Check if vault-credentials.txt was created -if [ -f "vault-credentials.txt" ]; then - log "INFO" "Credentials file was created successfully" +# Check if vault-credentials.json was created +if [ -f "vault-credentials.json" ]; then + log "INFO" "JSON credentials file was created successfully" else - log "ERROR" "Credentials file was not created" + log "ERROR" "JSON credentials file was not created" exit 1 fi -# Verify the content of vault-credentials.txt -if grep -q "Unseal Keys:" vault-credentials.txt && grep -q "Root Token:" vault-credentials.txt; then - log "INFO" "Credentials file contains expected content" +# Verify the content of vault-credentials.json +if jq -e '.keys_base64 | length' vault-credentials.json >/dev/null && \ + jq -e '.root_token' vault-credentials.json >/dev/null; then + log "INFO" "JSON credentials file contains expected content" else - log "ERROR" "Credentials file doesn't contain expected content" + log "ERROR" "JSON credentials file doesn't contain expected content" exit 1 fi +# Also check for backward compatibility +if [ -f "vault-credentials.txt" ]; then + log "INFO" "Text credentials file was also created (for backward compatibility)" +fi + # Verify Vault is unsealed after initial setup vault_status=$(docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault status -format=json 2>/dev/null || echo '{"sealed": true}') @@ -157,28 +163,35 @@ else echo $vault_status fi -# Extract keys from credentials file and root token -log "INFO" "Extracting unseal keys and root token from credentials file..." -unseal_keys=$(grep "Base64 Unseal Keys:" -A 3 vault-credentials.txt | grep "Key" | awk '{print $3}') -root_token=$(grep "Root Token:" vault-credentials.txt | awk '{print $3}') +# Extract keys from JSON credentials file +log "INFO" "Extracting unseal keys and root token from JSON credentials file..." +# Using jq to extract the first 3 unseal keys (as that's the threshold) +unseal_keys=$(jq -r '.keys_base64[0:3][]' vault-credentials.json) +root_token=$(jq -r '.root_token' vault-credentials.json) # First, try running 'vault operator unseal' directly for a more robust test log "INFO" "Attempting to unseal Vault directly with unseal keys..." -key1=$(echo "$unseal_keys" | head -n 1) -key2=$(echo "$unseal_keys" | head -n 2 | tail -n 1) -key3=$(echo "$unseal_keys" | head -n 3 | tail -n 1) +# Using an array to capture the keys +readarray -t key_array <<< "$unseal_keys" -docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal "$key1" -docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal "$key2" -docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal "$key3" +for key in "${key_array[@]}"; do + log "INFO" "Applying unseal key: ${key:0:8}..." # Show only first 8 chars for security + docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal "$key" +done # As a fallback, also try running vault-init with environment variables log "INFO" "Starting vault-init with environment variables..." -docker-compose run -e VAULT_ADDR=http://vault:8200 \ - -e VAULT_UNSEAL_KEY_1=$(echo "$unseal_keys" | head -n 1) \ - -e VAULT_UNSEAL_KEY_2=$(echo "$unseal_keys" | head -n 2 | tail -n 1) \ - -e VAULT_UNSEAL_KEY_3=$(echo "$unseal_keys" | head -n 3 | tail -n 1) \ - --rm vault-init +# Check how many keys we have +key_count=${#key_array[@]} +env_vars="-e VAULT_ADDR=http://vault:8200" + +# Add each key to environment variables +for i in $(seq 0 $((key_count-1))); do + env_vars="$env_vars -e VAULT_UNSEAL_KEY_$((i+1))=${key_array[$i]}" +done + +# Run the command with all environment variables +docker-compose run $env_vars --rm vault-init # Verify Vault is unsealed now vault_status=$(docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault status -format=json 2>/dev/null || echo '{"sealed": true}') @@ -197,9 +210,16 @@ fi # Test some basic Vault operations log "INFO" "Testing basic Vault operations..." -# Write a secret +# Write a secret using the root token from JSON credentials token_result=$(docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault login "$root_token" 2>&1) -log "INFO" "Login result: $(echo "$token_result" | grep "Success")" +login_success=$(echo "$token_result" | grep -c "Success" || echo "0") +if [ "$login_success" -gt 0 ]; then + log "INFO" "Successfully logged in with root token" +else + log "ERROR" "Failed to log in with root token" + echo "$token_result" + exit 1 +fi # Enable KV secrets engine enable_result=$(docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault secrets enable -path=kv kv 2>&1 || echo "KV already enabled")