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.
This commit is contained in:
Harald Hoyer 2025-03-20 15:31:04 +01:00
parent c3902ff0f1
commit 430970b375
4 changed files with 437 additions and 415 deletions

View file

@ -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;

View file

@ -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

173
src/vault_init.rs Normal file
View file

@ -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)
}

View file

@ -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<()> {