From 430970b3757e2ec60bfe021479b2cc68a7191da9 Mon Sep 17 00:00:00 2001
From: Harald Hoyer <harald@hoyer.xyz>
Date: Thu, 20 Mar 2025 15:31:04 +0100
Subject: [PATCH] feat(vault): add automated Vault initialization and unseal

- Introduced `initialize_vault` function to handle Vault setup, including health checks, initialization, and unsealing.
- Moved Vault-related logic into dedicated modules (`vault_init` and `vault_setup`) for cleaner separation of concerns.
- Simplified `main.rs` by delegating Vault initialization to a modular function.
---
 src/lib.rs         |   2 +
 src/main.rs        | 421 +--------------------------------------------
 src/vault_init.rs  | 173 +++++++++++++++++++
 src/vault_setup.rs | 256 +++++++++++++++++++++++++++
 4 files changed, 437 insertions(+), 415 deletions(-)
 create mode 100644 src/vault_init.rs

diff --git a/src/lib.rs b/src/lib.rs
index d0da82c..7ce0e6f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,9 +1,11 @@
 // Modules that implement our hierarchical signing system
 pub mod vault_setup;
+pub mod vault_init;
 pub mod document_service;
 pub mod api;
 
 // Re-export main components for easier access
 pub use vault_setup::VaultClient;
+pub use vault_init::initialize_vault;
 pub use document_service::DocumentService;
 pub use api::start_api;
diff --git a/src/main.rs b/src/main.rs
index e4eeed6..2efb99b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,264 +1,10 @@
-use anyhow::{Context, Result};
-use reqwest::{Client, StatusCode};
-use serde::{Deserialize, Serialize};
-use std::{
-    env,
-    fs::File,
-    io::Write,
-    path::Path,
-    time::Duration,
-};
-use tokio::time::sleep;
-use tracing::{info, warn, error, debug, instrument};
+use anyhow::Result;
+use std::env;
+use tracing::{info};
 use tracing_subscriber::{fmt, EnvFilter};
 
 // Import our library
-use vault_hier::start_api;
-
-// Vault API response structures
-#[derive(Debug, Deserialize)]
-struct InitResponse {
-    keys: Vec<String>,
-    keys_base64: Vec<String>,
-    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<()> {
-    // 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())?;
-        info!("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() {
-        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)?;
-
-    info!("Credentials saved to {}", file_path);
-    Ok(())
-}
-
-// Wait for Vault to become available
-#[instrument(skip(addr))]
-async fn wait_for_vault(addr: &str) -> Result<()> {
-    info!("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) {
-                    info!("Vault is available! Status code: {}", status);
-                    return Ok(());
-                }
-
-                debug!("Vault returned unexpected status code: {}", status);
-            },
-            Err(e) => {
-                debug!("Error connecting to Vault: {}", e);
-            }
-        }
-
-        if i == 30 {
-            error!("Timed out waiting for Vault to become available");
-            return Err(anyhow::anyhow!("Timed out waiting for Vault to become available"));
-        }
-
-        info!("Vault is unavailable - sleeping (attempt {}/30)", i);
-        sleep(Duration::from_secs(2)).await;
-    }
-
-    Ok(())
-}
-
-#[instrument(skip(client, addr))]
-async fn check_init_status(client: &Client, addr: &str) -> Result<bool> {
-    info!("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::<serde_json::Value>().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)
-}
-
-#[instrument(skip(client, addr))]
-async fn check_seal_status(client: &Client, addr: &str) -> Result<SealStatusResponse> {
-    info!("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::<SealStatusResponse>().await?;
-        info!("Seal status: sealed={}, threshold={}, shares={}, progress={}",
-            status.sealed, status.t, status.n, status.progress);
-        return Ok(status);
-    } else {
-        let error_text = response.text().await?;
-        error!("Failed to get seal status: {}", error_text);
-        anyhow::bail!("Failed to get seal status: {}", error_text);
-    }
-}
-
-#[instrument(skip(client, addr))]
-async fn init_vault(client: &Client, addr: &str) -> Result<InitResponse> {
-    // First check if already initialized
-    let initialized = check_init_status(client, addr).await?;
-
-    if initialized {
-        error!("Vault is already initialized. Cannot re-initialize.");
-        anyhow::bail!("Vault is already initialized. Cannot re-initialize.");
-    }
-
-    info!("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::<InitResponse>().await?;
-            info!("Vault initialized successfully!");
-            Ok(init_response)
-        }
-        status => {
-            let error_text = response.text().await?;
-            error!("Failed to initialize Vault: {} - {}", status, error_text);
-            anyhow::bail!("Failed to initialize Vault: {} - {}", status, error_text);
-        }
-    }
-}
-
-#[instrument(skip(client, addr, unseal_keys))]
-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 {
-        info!("Vault is already unsealed!");
-        return Ok(());
-    }
-
-    info!("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 {
-        error!(
-            "Not enough unseal keys provided. Need {} keys, but only have {}",
-            required_keys,
-            unseal_keys.len()
-        );
-        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() {
-        info!("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?;
-            error!("Failed to apply unseal key: {}", error_text);
-            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 {
-            info!("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 {
-        error!("Applied all available unseal keys, but Vault is still sealed");
-        anyhow::bail!("Applied all available unseal keys, but Vault is still sealed");
-    }
-
-    Ok(())
-}
+use vault_hier::{start_api, initialize_vault};
 
 #[tokio::main]
 async fn main() -> Result<()> {
@@ -270,7 +16,6 @@ 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();
 
     // Get API port from env var or use default
     let api_port = env::var("API_PORT")
@@ -281,163 +26,9 @@ async fn main() -> Result<()> {
     info!("Vault address: {}", vault_addr);
     info!("Connecting to Vault at: {}", vault_addr);
 
-    // Wait for Vault to be available
-    wait_for_vault(&vault_addr).await?;
+    // Initialize and unseal Vault, get the root token
+    let root_token = initialize_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?;
-                info!("Vault status: {}", status_text);
-            }
-        },
-        Err(e) => warn!("Error getting Vault status: {}", e),
-    }
-
-    // First check if Vault is already initialized
-    let initialized = check_init_status(&client, &vault_addr).await?;
-    let mut root_token = String::new();
-
-    if initialized {
-        info!("Vault is already initialized.");
-
-        // Check if Vault is sealed
-        let seal_status = check_seal_status(&client, &vault_addr).await?;
-
-        if seal_status.sealed {
-            info!("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) => {
-                        info!("Found unseal key {} from environment", i);
-                        unseal_keys.push(key);
-                    },
-                    Err(_) => {
-                        debug!("Unseal key {} not found in environment", i);
-                    }
-                }
-            }
-
-            // If we have unseal keys, try to unseal
-            if !unseal_keys.is_empty() {
-                info!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len());
-                unseal_vault(&client, &vault_addr, &unseal_keys).await?;
-            } else {
-                warn!("No unseal keys found. Vault remains sealed.");
-                info!("To unseal, set VAULT_UNSEAL_KEY_1, VAULT_UNSEAL_KEY_2, etc. environment variables.");
-            }
-        } else {
-            info!("Vault is already unsealed.");
-        }
-
-        // Try to load root token from environment or credentials file
-        match env::var("VAULT_TOKEN") {
-            Ok(token) => {
-                info!("Found root token from environment");
-                root_token = token;
-            },
-            Err(_) => {
-                // Try to load from credentials file
-                if let Ok(contents) = std::fs::read_to_string("vault-credentials.json") {
-                    if let Ok(creds) = serde_json::from_str::<serde_json::Value>(&contents) {
-                        if let Some(token) = creds["root_token"].as_str() {
-                            info!("Found root token from credentials file");
-                            root_token = token.to_string();
-                        }
-                    }
-                }
-            }
-        }
-
-        if root_token.is_empty() {
-            error!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file.");
-            anyhow::bail!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file.");
-        }
-    } else {
-        // Initialize Vault
-        info!("Vault is not initialized. Proceeding with initialization...");
-        let init_response = init_vault(&client, &vault_addr).await?;
-
-        // Save credentials to files
-        info!("Saving credentials to files...");
-        let current_dir = std::env::current_dir().context("Failed to get current directory")?;
-
-        // Save as JSON (new format)
-        let json_path = current_dir.join("vault-credentials.json");
-        save_credentials(&init_response, json_path.to_str().unwrap())?;
-        info!("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())?;
-        info!("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_json_path = "/app/data/vault-credentials.json";
-            save_credentials(&init_response, docker_json_path)?;
-            info!("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)?;
-            info!("Backup text credentials saved to Docker volume at: {}", docker_text_path);
-        }
-
-        info!("=========================================");
-        info!("IMPORTANT: SAVE THESE CREDENTIALS SECURELY");
-        info!("=========================================");
-        info!("Root Token: {}", init_response.root_token);
-        info!("Unseal Keys (first 3 of 5 needed to unseal):");
-        for (i, key) in init_response.keys_base64.iter().enumerate() {
-            info!("Key {}: {}", i + 1, key);
-        }
-        info!("=========================================");
-
-        // 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::<Vec<String>>();
-
-        unseal_vault(&client, &vault_addr, &unseal_keys).await?;
-
-        info!("Vault initialization and unseal complete!");
-
-        // Set root token
-        root_token = init_response.root_token;
-    }
-
-    // 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() {
-            info!("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(_) => info!("JSON credentials saved to Docker volume"),
-                    Err(e) => warn!("Failed to copy JSON credentials: {}", e),
-                }
-            }
-        }
-    }
-
-    if let Ok(metadata) = std::fs::metadata("vault-credentials.txt") {
-        if metadata.is_file() {
-            info!("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(_) => info!("Text credentials saved to Docker volume"),
-                    Err(e) => warn!("Failed to copy text credentials: {}", e),
-                }
-            }
-        }
-    }
-
-    info!("Vault setup complete!");
     info!("Starting hierarchical document signing API...");
 
     // Start the hierarchical signing API
diff --git a/src/vault_init.rs b/src/vault_init.rs
new file mode 100644
index 0000000..9d489e1
--- /dev/null
+++ b/src/vault_init.rs
@@ -0,0 +1,173 @@
+use anyhow::{Context, Result};
+use reqwest::Client;
+use std::{
+    env,
+    fs,
+};
+use tracing::{info, warn, error, debug};
+
+use crate::vault_setup::VaultClient;
+
+/// Initialize and unseal the Vault, returning the root token for further operations
+pub async fn initialize_vault(vault_addr: &str) -> Result<String> {
+    let client = Client::new();
+
+    // Wait for Vault to be available
+    VaultClient::wait_for_vault(vault_addr).await?;
+
+    // Display Vault health status
+    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?;
+                info!("Vault status: {}", status_text);
+            }
+        },
+        Err(e) => warn!("Error getting Vault status: {}", e),
+    }
+
+    // First check if Vault is already initialized
+    let initialized = VaultClient::check_init_status(&client, vault_addr).await?;
+    let mut root_token = String::new();
+
+    if initialized {
+        info!("Vault is already initialized.");
+
+        // Check if Vault is sealed
+        let seal_status = VaultClient::check_seal_status(&client, vault_addr).await?;
+
+        if seal_status.sealed {
+            info!("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) => {
+                        info!("Found unseal key {} from environment", i);
+                        unseal_keys.push(key);
+                    },
+                    Err(_) => {
+                        debug!("Unseal key {} not found in environment", i);
+                    }
+                }
+            }
+
+            // If we have unseal keys, try to unseal
+            if !unseal_keys.is_empty() {
+                info!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len());
+                VaultClient::unseal_vault(&client, vault_addr, &unseal_keys).await?;
+            } else {
+                warn!("No unseal keys found. Vault remains sealed.");
+                info!("To unseal, set VAULT_UNSEAL_KEY_1, VAULT_UNSEAL_KEY_2, etc. environment variables.");
+            }
+        } else {
+            info!("Vault is already unsealed.");
+        }
+
+        // Try to load root token from environment or credentials file
+        match env::var("VAULT_TOKEN") {
+            Ok(token) => {
+                info!("Found root token from environment");
+                root_token = token;
+            },
+            Err(_) => {
+                // Try to load from credentials file
+                if let Ok(contents) = fs::read_to_string("vault-credentials.json") {
+                    if let Ok(creds) = serde_json::from_str::<serde_json::Value>(&contents) {
+                        if let Some(token) = creds["root_token"].as_str() {
+                            info!("Found root token from credentials file");
+                            root_token = token.to_string();
+                        }
+                    }
+                }
+            }
+        }
+
+        if root_token.is_empty() {
+            error!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file.");
+            anyhow::bail!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file.");
+        }
+    } else {
+        // Initialize Vault
+        info!("Vault is not initialized. Proceeding with initialization...");
+        let init_response = VaultClient::init_vault(&client, vault_addr).await?;
+
+        // Save credentials to files
+        info!("Saving credentials to files...");
+        let current_dir = std::env::current_dir().context("Failed to get current directory")?;
+
+        // Save as JSON (new format)
+        let json_path = current_dir.join("vault-credentials.json");
+        VaultClient::save_credentials(&init_response, json_path.to_str().unwrap())?;
+        info!("JSON credentials saved to: {}", json_path.display());
+
+        // Save as text (for backward compatibility)
+        let text_path = current_dir.join("vault-credentials.txt");
+        VaultClient::save_credentials(&init_response, text_path.to_str().unwrap())?;
+        info!("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_json_path = "/app/data/vault-credentials.json";
+            VaultClient::save_credentials(&init_response, docker_json_path)?;
+            info!("Backup JSON credentials saved to Docker volume at: {}", docker_json_path);
+
+            let docker_text_path = "/app/data/vault-credentials.txt";
+            VaultClient::save_credentials(&init_response, docker_text_path)?;
+            info!("Backup text credentials saved to Docker volume at: {}", docker_text_path);
+        }
+
+        info!("=========================================");
+        info!("IMPORTANT: SAVE THESE CREDENTIALS SECURELY");
+        info!("=========================================");
+        info!("Root Token: {}", init_response.root_token);
+        info!("Unseal Keys (first 3 of 5 needed to unseal):");
+        for (i, key) in init_response.keys_base64.iter().enumerate() {
+            info!("Key {}: {}", i + 1, key);
+        }
+        info!("=========================================");
+
+        // 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::<Vec<String>>();
+
+        VaultClient::unseal_vault(&client, vault_addr, &unseal_keys).await?;
+
+        info!("Vault initialization and unseal complete!");
+
+        // Set root token
+        root_token = init_response.root_token;
+    }
+
+    // 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() {
+            info!("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(_) => info!("JSON credentials saved to Docker volume"),
+                    Err(e) => warn!("Failed to copy JSON credentials: {}", e),
+                }
+            }
+        }
+    }
+
+    if let Ok(metadata) = std::fs::metadata("vault-credentials.txt") {
+        if metadata.is_file() {
+            info!("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(_) => info!("Text credentials saved to Docker volume"),
+                    Err(e) => warn!("Failed to copy text credentials: {}", e),
+                }
+            }
+        }
+    }
+
+    info!("Vault setup complete!");
+    Ok(root_token)
+}
diff --git a/src/vault_setup.rs b/src/vault_setup.rs
index c603d31..771b3cf 100644
--- a/src/vault_setup.rs
+++ b/src/vault_setup.rs
@@ -2,8 +2,42 @@ use anyhow::{Context, Result};
 use reqwest::{Client, StatusCode};
 use serde::{Deserialize, Serialize};
 use serde_json::json;
+use std::{
+    fs::File,
+    io::Write,
+    path::Path,
+    time::Duration,
+};
+use tokio::time::sleep;
 use tracing::{info, error, debug, instrument};
 
+// Vault API response structures
+#[derive(Debug, Deserialize)]
+pub struct InitResponse {
+    pub keys: Vec<String>,
+    pub keys_base64: Vec<String>,
+    pub root_token: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SealStatusResponse {
+    pub sealed: bool,
+    pub t: u8,
+    pub n: u8,
+    pub progress: u8,
+}
+
+#[derive(Debug, Serialize)]
+pub struct InitRequest {
+    pub secret_shares: u8,
+    pub secret_threshold: u8,
+}
+
+#[derive(Debug, Serialize)]
+pub struct UnsealRequest {
+    pub key: String,
+}
+
 // Department types for organizational structure
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub enum Department {
@@ -35,6 +69,228 @@ impl VaultClient {
         }
     }
 
+    // Wait for Vault to become available
+    #[instrument(skip(addr))]
+    pub async fn wait_for_vault(addr: &str) -> Result<()> {
+        info!("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) {
+                        info!("Vault is available! Status code: {}", status);
+                        return Ok(());
+                    }
+
+                    debug!("Vault returned unexpected status code: {}", status);
+                },
+                Err(e) => {
+                    debug!("Error connecting to Vault: {}", e);
+                }
+            }
+
+            if i == 30 {
+                error!("Timed out waiting for Vault to become available");
+                return Err(anyhow::anyhow!("Timed out waiting for Vault to become available"));
+            }
+
+            info!("Vault is unavailable - sleeping (attempt {}/30)", i);
+            sleep(Duration::from_secs(2)).await;
+        }
+
+        Ok(())
+    }
+
+    // Function to check if Vault is initialized
+    #[instrument(skip(client, addr))]
+    pub async fn check_init_status(client: &Client, addr: &str) -> Result<bool> {
+        info!("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::<serde_json::Value>().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)
+    }
+
+    // Function to check Vault seal status
+    #[instrument(skip(client, addr))]
+    pub async fn check_seal_status(client: &Client, addr: &str) -> Result<SealStatusResponse> {
+        info!("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::<SealStatusResponse>().await?;
+            info!("Seal status: sealed={}, threshold={}, shares={}, progress={}",
+                status.sealed, status.t, status.n, status.progress);
+            return Ok(status);
+        } else {
+            let error_text = response.text().await?;
+            error!("Failed to get seal status: {}", error_text);
+            anyhow::bail!("Failed to get seal status: {}", error_text);
+        }
+    }
+
+    // Function to initialize Vault
+    #[instrument(skip(client, addr))]
+    pub async fn init_vault(client: &Client, addr: &str) -> Result<InitResponse> {
+        // First check if already initialized
+        let initialized = Self::check_init_status(client, addr).await?;
+
+        if initialized {
+            error!("Vault is already initialized. Cannot re-initialize.");
+            anyhow::bail!("Vault is already initialized. Cannot re-initialize.");
+        }
+
+        info!("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::<InitResponse>().await?;
+                info!("Vault initialized successfully!");
+                Ok(init_response)
+            }
+            status => {
+                let error_text = response.text().await?;
+                error!("Failed to initialize Vault: {} - {}", status, error_text);
+                anyhow::bail!("Failed to initialize Vault: {} - {}", status, error_text);
+            }
+        }
+    }
+
+    // Function to unseal Vault
+    #[instrument(skip(client, addr, unseal_keys))]
+    pub async fn unseal_vault(client: &Client, addr: &str, unseal_keys: &[String]) -> Result<()> {
+        // First check the current seal status
+        let mut seal_status = Self::check_seal_status(client, addr).await?;
+
+        if !seal_status.sealed {
+            info!("Vault is already unsealed!");
+            return Ok(());
+        }
+
+        info!("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 {
+            error!(
+                "Not enough unseal keys provided. Need {} keys, but only have {}",
+                required_keys,
+                unseal_keys.len()
+            );
+            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() {
+            info!("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?;
+                error!("Failed to apply unseal key: {}", error_text);
+                anyhow::bail!("Failed to apply unseal key: {}", error_text);
+            }
+
+            // Check the updated seal status
+            seal_status = Self::check_seal_status(client, addr).await?;
+
+            if !seal_status.sealed {
+                info!("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 {
+            error!("Applied all available unseal keys, but Vault is still sealed");
+            anyhow::bail!("Applied all available unseal keys, but Vault is still sealed");
+        }
+
+        Ok(())
+    }
+
+    // Function to save Vault credentials to a file
+    pub 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())?;
+            info!("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() {
+            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)?;
+
+        info!("Credentials saved to {}", file_path);
+        Ok(())
+    }
+
     // Enable required secrets engines
     #[instrument(skip(self))]
     pub async fn setup_secrets_engines(&self) -> Result<()> {