commit 07cf031bbbfb499ea5867fceb5f55475dbda09fe Author: Harald Hoyer Date: Thu Mar 20 12:49:44 2025 +0100 Initial commit: Vault Hierarchical Initializer This commit adds the full implementation of vault-hier, a Rust utility for: - Initializing HashiCorp Vault in production mode (non-dev) - Handling Vault seal/unseal operations with key thresholds - Using Docker Compose for containerized operation - Supporting persistent storage via Docker volumes Key components: - Rust application for Vault interaction - Docker and Docker Compose configuration - Test scripts for local development - Nix flake for development dependencies 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0054e88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Rust build artifacts +/target/ +**/*.rs.bk +*.pdb +Cargo.lock + +# Generated by Cargo +.cargo/ + +# Direnv +.direnv/ +.envrc + +# Vault related files +vault-credentials.txt +vault-config/ + +# Temporary test files +docker-compose-test.yml +test_vault.sh + +# IDE files +.idea/ +.vscode/ +*.iml +*.swp +*~ + +# Nix +result +result-* + +# macOS specific files +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3382a3f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vault-hier" +version = "0.1.0" +edition = "2024" + +[dependencies] +reqwest = { version = "0.11.18", features = ["json"] } +tokio = { version = "1.28.0", features = ["full"] } +serde = { version = "1.0.160", features = ["derive"] } +serde_json = "1.0.96" +anyhow = "1.0.70" + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8bf3e59 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM rust:1.85-bookworm AS builder + +WORKDIR /usr/src/vault-hier +COPY Cargo.toml . +COPY src src + +# Create a dummy main.rs to build dependencies +RUN mkdir -p .cargo && \ + cargo build --release && \ + rm -rf src target/release/deps/vault_hier* + +# Build the actual application +COPY . . +RUN cargo build --release + +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y \ + ca-certificates \ + curl \ + gnupg \ + lsb-release \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Install Vault +RUN wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg +RUN echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/hashicorp.list +RUN apt-get update && apt-get install -y vault + +WORKDIR /usr/local/bin + +COPY --from=builder /usr/src/vault-hier/target/release/vault-hier . +# Set the entrypoint to directly run the Rust binary +ENTRYPOINT ["/usr/local/bin/vault-hier"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..06b65ad --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Vault Hierarchical Initializer + +A Rust-based utility for initializing and unsealing HashiCorp Vault in non-dev (production) mode. + +## Overview + +This project provides a Docker-based solution for: + +1. Running a HashiCorp Vault server in non-dev (production) mode +2. Automatically initializing the Vault instance +3. Unsealing the Vault after initialization +4. Storing unseal keys and root token securely + +## Prerequisites + +- Docker and Docker Compose installed on your system +- Rust (if you want to build the project locally) + +## Configuration + +In production mode, Vault: +- Starts sealed and requires a threshold of unseal keys to unseal +- Stores data persistently in mounted volumes +- Requires explicit initialization +- Needs manual unsealing after restarts + +The implementation uses: +- 5 key shares with a threshold of 3 keys needed for unsealing +- Persistent volume storage for Vault data + +## Usage + +### Starting Vault with Docker Compose + +```bash +docker-compose up -d +``` + +This will: +1. Start a Vault server in production mode +2. Run the vault-hier utility to initialize Vault if needed +3. Automatically unseal Vault using the threshold number of keys +4. Save the unseal keys and root token to `vault-credentials.txt` in the mounted volume + +### Getting Vault Credentials + +After initialization, you can find the unseal keys and root token in: + +``` +./vault-credentials.txt +``` + +Keep these credentials safe! They provide full access to your Vault instance. + +### Restarting a Sealed Vault + +If your Vault instance restarts, it will start in a sealed state. To unseal it automatically: + +```bash +# Set the unseal keys as environment variables +export VAULT_UNSEAL_KEY_1="your-first-key" +export VAULT_UNSEAL_KEY_2="your-second-key" +export VAULT_UNSEAL_KEY_3="your-third-key" + +# Restart the vault-init container to trigger unsealing +docker-compose restart vault-init +``` + +## Development + +### Building the Project Locally + +```bash +cargo build --release +``` + +### Running Tests + +```bash +cargo test +``` + +### Custom Configuration + +To modify the key sharing threshold: +1. Edit the `init_req` struct in `src/main.rs` +2. Rebuild the Docker image + +## Security Considerations + +- In a production environment, never store unseal keys on the same machine as Vault +- Consider using a key management solution like Shamir's Secret Sharing +- Rotate root tokens regularly and use appropriate authentication methods \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a01ee30 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +services: + vault: + image: hashicorp/vault:1.15 + container_name: vault + ports: + - "8200:8200" + environment: + - 'VAULT_LOCAL_CONFIG={"storage": {"file": {"path": "/vault/file"}}, "listener": {"tcp": {"address": "0.0.0.0:8200", "tls_disable": true}}, "ui": true, "disable_mlock": true}' + cap_add: + - IPC_LOCK + volumes: + - vault-data:/vault/file + command: server + healthcheck: + test: ["CMD", "sh", "-c", "wget -q -O- --no-check-certificate http://127.0.0.1:8200/v1/sys/health?standbyok=true\\&sealedok=true\\&uninitok=true || exit 0"] + interval: 5s + timeout: 2s + retries: 3 + start_period: 5s + networks: + - vault-net + + vault-init: + build: + context: . + dockerfile: Dockerfile + container_name: vault-init + environment: + - VAULT_ADDR=http://vault:8200 + depends_on: + vault: + condition: service_healthy + volumes: + - ./:/app/data + networks: + - vault-net + restart: on-failure + # Using a non-daemon container that exits after completion + deploy: + restart_policy: + condition: none + +volumes: + vault-data: + +networks: + vault-net: + driver: bridge \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4b258a6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1742268799, + "narHash": "sha256-IhnK4LhkBlf14/F8THvUy3xi/TxSQkp9hikfDZRD4Ic=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da044451c6a70518db5b730fe277b70f494188f1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1736320768, + "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1742437918, + "narHash": "sha256-Vflb6KJVDikFcM9E231mRN88uk4+jo7BWtaaQMifthI=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "f03085549609e49c7bcbbee86a1949057d087199", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d774b81 --- /dev/null +++ b/flake.nix @@ -0,0 +1,43 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay.url = "github:oxalica/rust-overlay"; + }; + outputs = { self, nixpkgs, flake-utils, rust-overlay }: + flake-utils.lib.eachDefaultSystem + (system: + let + overlays = [ + rust-overlay.overlays.default + ]; + pkgs = import nixpkgs { + inherit system overlays; + config = { + allowUnfree = true; + }; + }; + in + with pkgs; + { + devShells.default = mkShell { + env = { + OPENSSL_NO_VENDOR = "1"; + NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa"; + }; + + packages = [ + pkg-config + vault + (rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + }) + rustc + cargo + rustfmt + clippy + ]; + }; + } + ); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2460556 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,412 @@ +use anyhow::{Context, Result}; +use reqwest::{Client, StatusCode}; +use serde::{Deserialize, Serialize}; +use std::{ + env, + fs::File, + io::Write, + path::Path, + process::Command, + time::Duration, +}; +use tokio::time::sleep; + +// Vault API response structures +#[derive(Debug, Deserialize)] +struct InitResponse { + keys: Vec, + keys_base64: Vec, + root_token: String, +} + +#[derive(Debug, Deserialize)] +struct SealStatusResponse { + sealed: bool, + t: u8, + n: u8, + progress: u8, +} + +#[derive(Debug, Serialize)] +struct InitRequest { + secret_shares: u8, + secret_threshold: u8, +} + +#[derive(Debug, Serialize)] +struct UnsealRequest { + key: String, +} + +// Function to save Vault credentials to a file +fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> { + let mut file = File::create(Path::new(file_path))?; + writeln!(file, "Unseal Keys:")?; + for (i, key) in response.keys.iter().enumerate() { + writeln!(file, "Key {}: {}", i + 1, key)?; + } + writeln!(file, "Base64 Unseal Keys:")?; + for (i, key) in response.keys_base64.iter().enumerate() { + writeln!(file, "Key {}: {}", i + 1, key)?; + } + writeln!(file)?; + writeln!(file, "Root Token: {}", response.root_token)?; + + println!("Credentials saved to {}", file_path); + Ok(()) +} + +// 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(); + // Accept any of these status codes as "available" + if matches!(status, 200 | 429 | 472 | 473 | 501 | 503) { + 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={}", + status.sealed, status.t, status.n, status.progress); + return Ok(status); + } else { + let error_text = response.text().await?; + anyhow::bail!("Failed to get seal status: {}", error_text); + } +} + +async fn init_vault(client: &Client, addr: &str) -> 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?; + println!("Vault initialized successfully!"); + Ok(init_response) + } + status => { + let error_text = response.text().await?; + anyhow::bail!("Failed to initialize Vault: {} - {}", status, error_text); + } + } +} + +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, + 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(()) +} + +#[tokio::main] +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 { + Ok(response) => { + if response.status().is_success() { + let status_text = response.text().await?; + println!("Vault status: {}", status_text); + } + }, + 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 { + match env::var(format!("VAULT_UNSEAL_KEY_{}", i)) { + Ok(key) => { + println!("Found unseal key {} from environment", i); + unseal_keys.push(key); + }, + Err(_) => { + println!("Unseal key {} not found in environment", i); + } + } + } + + // If we have unseal keys, try to unseal + if !unseal_keys.is_empty() { + println!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len()); + unseal_vault(&client, &vault_addr, &unseal_keys).await?; + } else { + println!("No unseal keys found. Vault remains sealed."); + println!("To unseal, set VAULT_UNSEAL_KEY_1, VAULT_UNSEAL_KEY_2, etc. environment variables."); + } + } else { + println!("Vault is already unsealed."); + } + } else { + // 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..."); + 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()); + + // 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); + } + + println!("========================================="); + println!("IMPORTANT: SAVE THESE CREDENTIALS SECURELY"); + println!("========================================="); + println!("Root Token: {}", init_response.root_token); + println!("Unseal Keys (first 3 of 5 needed to unseal):"); + for (i, key) in init_response.keys_base64.iter().enumerate() { + 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")?; + + println!("Operation complete!"); + + Ok(()) +} \ No newline at end of file diff --git a/test_local.sh b/test_local.sh new file mode 100755 index 0000000..903eb64 --- /dev/null +++ b/test_local.sh @@ -0,0 +1,118 @@ +#!/bin/bash +set -e + +# Detect OS and handle accordingly +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + export VAULT_ADDR="http://127.0.0.1:8200" + VAULT_PID_FILE="/tmp/vault.pid" +elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Linux + export VAULT_ADDR="http://127.0.0.1:8200" + VAULT_PID_FILE="/tmp/vault.pid" +else + # Windows or other + export VAULT_ADDR="http://127.0.0.1:8200" + VAULT_PID_FILE="./vault.pid" +fi + +# Check if Vault is installed +if ! command -v vault &> /dev/null; then + echo "Vault is not installed. Please install it first." + exit 1 +fi + +# Check if there's already a Vault process running +if [ -f "$VAULT_PID_FILE" ]; then + VAULT_PID=$(cat "$VAULT_PID_FILE") + if ps -p $VAULT_PID > /dev/null; then + echo "Vault is already running with PID $VAULT_PID" + echo "Stopping the existing Vault server..." + kill -9 $VAULT_PID + rm "$VAULT_PID_FILE" + # Wait for the port to be released + sleep 2 + else + echo "Vault PID file exists but the process is not running. Removing stale PID file." + rm "$VAULT_PID_FILE" + fi +fi + +echo "Starting Vault server in non-dev mode..." + +# Create temporary config file +mkdir -p /tmp/vault-test/data /tmp/vault-test/config + +cat > /tmp/vault-test/config/vault.hcl << EOF +storage "file" { + path = "/tmp/vault-test/data" +} + +listener "tcp" { + address = "127.0.0.1:8200" + tls_disable = "true" +} + +disable_mlock = true +ui = true +EOF + +vault server -config=/tmp/vault-test/config/vault.hcl > ./vault_server.log 2>&1 & +VAULT_PID=$! +echo $VAULT_PID > "$VAULT_PID_FILE" + +echo "Vault server started with PID $VAULT_PID" +echo "Vault server is running at $VAULT_ADDR" + +# Wait for Vault to start +echo "Waiting for Vault to start..." +sleep 5 + +# Check if Vault is up and running +for i in {1..10}; do + if curl -fs -m 1 http://127.0.0.1:8200/v1/sys/health?standbyok=true\&sealedok=true\&uninitok=true > /dev/null 2>&1; then + echo "Vault is up and running!" + break + fi + + if [ $i -eq 10 ]; then + echo "Timed out waiting for Vault to become available" + exit 1 + fi + + echo "Vault is unavailable - sleeping (attempt $i/10)" + sleep 2 +done + +# Build and run the Rust application +echo "Building and running the Rust application..." +cargo build && cargo run + +# Check if the credentials file was created +if [ -f "vault-credentials.txt" ]; then + echo "Test successful! Credentials were saved to vault-credentials.txt" + # Extract the unseal keys for demonstration + UNSEAL_KEYS=$(grep "Key" vault-credentials.txt | head -n 3 | awk '{print $3}') + ROOT_TOKEN=$(grep "Root Token" vault-credentials.txt | awk '{print $3}') + + echo "Root Token: $ROOT_TOKEN" + echo "First 3 Unseal Keys (needed for threshold):" + echo "$UNSEAL_KEYS" + + # Clean up temporary files + rm -f vault-credentials.txt +else + echo "Test failed! Credentials file was not created." + exit 1 +fi + +echo -e "\nTest complete! Cleaning up..." +# Stop Vault server +kill -9 $VAULT_PID +rm "$VAULT_PID_FILE" + +# Clean up test environment +rm -rf /tmp/vault-test +rm -f ./vault_server.log + +echo "All cleaned up. Test successful!" \ No newline at end of file