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:
commit
07cf031bbb
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal 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
12
Cargo.toml
Normal 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
35
Dockerfile
Normal 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
93
README.md
Normal 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
48
docker-compose.yml
Normal 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
96
flake.lock
Normal 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
43
flake.nix
Normal 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
412
src/main.rs
Normal 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
118
test_local.sh
Executable 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!"
|
Loading…
Reference in a new issue