Implement JSON credential storage

- Add JSON format for storing Vault credentials
- Update save_credentials function to support both formats
- Save both .json and .txt files for compatibility
- Update test_docker.sh to use jq for reliable JSON parsing
- Improve key extraction for unseal operations
- Update .gitignore to exclude JSON credentials

🤖 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 13:16:39 +01:00
parent 98384791c3
commit 9b3ac63c3e
3 changed files with 150 additions and 159 deletions

1
.gitignore vendored
View file

@ -13,6 +13,7 @@ Cargo.lock
# Vault related files
vault-credentials.txt
vault-credentials.json
vault-config/
# Temporary test files

View file

@ -6,7 +6,6 @@ use std::{
fs::File,
io::Write,
path::Path,
process::Command,
time::Duration,
};
use tokio::time::sleep;
@ -40,6 +39,21 @@ struct UnsealRequest {
// 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())?;
println!("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() {
@ -51,7 +65,7 @@ fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> {
}
writeln!(file)?;
writeln!(file, "Root Token: {}", response.root_token)?;
println!("Credentials saved to {}", file_path);
Ok(())
}
@ -59,12 +73,12 @@ fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> {
// 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();
@ -73,121 +87,55 @@ async fn wait_for_vault(addr: &str) -> Result<()> {
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={}",
println!("Seal status: sealed={}, threshold={}, shares={}, progress={}",
status.sealed, status.t, status.n, status.progress);
return Ok(status);
} else {
@ -199,26 +147,26 @@ async fn check_seal_status(client: &Client, addr: &str) -> Result<SealStatusResp
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?;
@ -235,59 +183,59 @@ async fn init_vault(client: &Client, addr: &str) -> Result<InitResponse> {
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,
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(())
}
@ -296,13 +244,13 @@ 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 {
@ -314,19 +262,19 @@ async fn main() -> Result<()> {
},
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 {
@ -340,7 +288,7 @@ async fn main() -> Result<()> {
}
}
}
// If we have unseal keys, try to unseal
if !unseal_keys.is_empty() {
println!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len());
@ -356,21 +304,32 @@ async fn main() -> Result<()> {
// 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...");
println!("Saving credentials to files...");
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());
// Save as JSON (new format)
let json_path = current_dir.join("vault-credentials.json");
save_credentials(&init_response, json_path.to_str().unwrap())?;
println!("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())?;
println!("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_path = "/app/data/vault-credentials.txt";
save_credentials(&init_response, docker_path)?;
println!("Backup credentials saved to Docker volume at: {}", docker_path);
let docker_json_path = "/app/data/vault-credentials.json";
save_credentials(&init_response, docker_json_path)?;
println!("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)?;
println!("Backup text credentials saved to Docker volume at: {}", docker_text_path);
}
println!("=========================================");
println!("IMPORTANT: SAVE THESE CREDENTIALS SECURELY");
println!("=========================================");
@ -380,33 +339,44 @@ async fn main() -> Result<()> {
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")?;
// 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() {
println!("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(_) => println!("JSON credentials saved to Docker volume"),
Err(e) => println!("Failed to copy JSON credentials: {}", e),
}
}
}
}
if let Ok(metadata) = std::fs::metadata("vault-credentials.txt") {
if metadata.is_file() {
println!("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(_) => println!("Text credentials saved to Docker volume"),
Err(e) => println!("Failed to copy text credentials: {}", e),
}
}
}
}
println!("Operation complete!");
Ok(())
}
}

View file

@ -99,22 +99,28 @@ wait_for_vault_init() {
# Wait for vault-init to complete
wait_for_vault_init
# Check if vault-credentials.txt was created
if [ -f "vault-credentials.txt" ]; then
log "INFO" "Credentials file was created successfully"
# Check if vault-credentials.json was created
if [ -f "vault-credentials.json" ]; then
log "INFO" "JSON credentials file was created successfully"
else
log "ERROR" "Credentials file was not created"
log "ERROR" "JSON credentials file was not created"
exit 1
fi
# Verify the content of vault-credentials.txt
if grep -q "Unseal Keys:" vault-credentials.txt && grep -q "Root Token:" vault-credentials.txt; then
log "INFO" "Credentials file contains expected content"
# Verify the content of vault-credentials.json
if jq -e '.keys_base64 | length' vault-credentials.json >/dev/null && \
jq -e '.root_token' vault-credentials.json >/dev/null; then
log "INFO" "JSON credentials file contains expected content"
else
log "ERROR" "Credentials file doesn't contain expected content"
log "ERROR" "JSON credentials file doesn't contain expected content"
exit 1
fi
# Also check for backward compatibility
if [ -f "vault-credentials.txt" ]; then
log "INFO" "Text credentials file was also created (for backward compatibility)"
fi
# Verify Vault is unsealed after initial setup
vault_status=$(docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault status -format=json 2>/dev/null || echo '{"sealed": true}')
@ -157,28 +163,35 @@ else
echo $vault_status
fi
# Extract keys from credentials file and root token
log "INFO" "Extracting unseal keys and root token from credentials file..."
unseal_keys=$(grep "Base64 Unseal Keys:" -A 3 vault-credentials.txt | grep "Key" | awk '{print $3}')
root_token=$(grep "Root Token:" vault-credentials.txt | awk '{print $3}')
# Extract keys from JSON credentials file
log "INFO" "Extracting unseal keys and root token from JSON credentials file..."
# Using jq to extract the first 3 unseal keys (as that's the threshold)
unseal_keys=$(jq -r '.keys_base64[0:3][]' vault-credentials.json)
root_token=$(jq -r '.root_token' vault-credentials.json)
# First, try running 'vault operator unseal' directly for a more robust test
log "INFO" "Attempting to unseal Vault directly with unseal keys..."
key1=$(echo "$unseal_keys" | head -n 1)
key2=$(echo "$unseal_keys" | head -n 2 | tail -n 1)
key3=$(echo "$unseal_keys" | head -n 3 | tail -n 1)
# Using an array to capture the keys
readarray -t key_array <<< "$unseal_keys"
docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal "$key1"
docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal "$key2"
docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal "$key3"
for key in "${key_array[@]}"; do
log "INFO" "Applying unseal key: ${key:0:8}..." # Show only first 8 chars for security
docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal "$key"
done
# As a fallback, also try running vault-init with environment variables
log "INFO" "Starting vault-init with environment variables..."
docker-compose run -e VAULT_ADDR=http://vault:8200 \
-e VAULT_UNSEAL_KEY_1=$(echo "$unseal_keys" | head -n 1) \
-e VAULT_UNSEAL_KEY_2=$(echo "$unseal_keys" | head -n 2 | tail -n 1) \
-e VAULT_UNSEAL_KEY_3=$(echo "$unseal_keys" | head -n 3 | tail -n 1) \
--rm vault-init
# Check how many keys we have
key_count=${#key_array[@]}
env_vars="-e VAULT_ADDR=http://vault:8200"
# Add each key to environment variables
for i in $(seq 0 $((key_count-1))); do
env_vars="$env_vars -e VAULT_UNSEAL_KEY_$((i+1))=${key_array[$i]}"
done
# Run the command with all environment variables
docker-compose run $env_vars --rm vault-init
# Verify Vault is unsealed now
vault_status=$(docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault status -format=json 2>/dev/null || echo '{"sealed": true}')
@ -197,9 +210,16 @@ fi
# Test some basic Vault operations
log "INFO" "Testing basic Vault operations..."
# Write a secret
# Write a secret using the root token from JSON credentials
token_result=$(docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault login "$root_token" 2>&1)
log "INFO" "Login result: $(echo "$token_result" | grep "Success")"
login_success=$(echo "$token_result" | grep -c "Success" || echo "0")
if [ "$login_success" -gt 0 ]; then
log "INFO" "Successfully logged in with root token"
else
log "ERROR" "Failed to log in with root token"
echo "$token_result"
exit 1
fi
# Enable KV secrets engine
enable_result=$(docker-compose exec -T vault env VAULT_ADDR=http://127.0.0.1:8200 vault secrets enable -path=kv kv 2>&1 || echo "KV already enabled")