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 <noreply@anthropic.com>
This commit is contained in:
Harald Hoyer 2025-03-20 12:49:44 +01:00
commit 07cf031bbb
9 changed files with 891 additions and 0 deletions

34
.gitignore vendored Normal file
View file

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

12
Cargo.toml Normal file
View file

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

35
Dockerfile Normal file
View file

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

93
README.md Normal file
View file

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

48
docker-compose.yml Normal file
View file

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

96
flake.lock Normal file
View file

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

43
flake.nix Normal file
View file

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

412
src/main.rs Normal file
View file

@ -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<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<()> {
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<bool> {
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::<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)
}
async fn check_seal_status(client: &Client, addr: &str) -> Result<SealStatusResponse> {
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::<SealStatusResponse>().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<InitResponse> {
// 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::<InitResponse>().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::<Vec<String>>();
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(())
}

118
test_local.sh Executable file
View file

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