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