vault: implement bootstrap integration
This commit is contained in:
78
scripts/vault-fetch/README.md
Normal file
78
scripts/vault-fetch/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# vault-fetch
|
||||
|
||||
A helper script for fetching secrets from OpenBao/Vault and writing them to the filesystem.
|
||||
|
||||
## Features
|
||||
|
||||
- **AppRole Authentication**: Uses role_id and secret_id from `/var/lib/vault/approle/`
|
||||
- **Individual Secret Files**: Writes each secret key as a separate file for easy consumption
|
||||
- **Caching**: Maintains a cache of secrets for fallback when Vault is unreachable
|
||||
- **Graceful Degradation**: Falls back to cached secrets if Vault authentication fails
|
||||
- **Secure Permissions**: Sets 600 permissions on all secret files
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
vault-fetch <secret-path> <output-directory> [cache-directory]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Fetch Grafana admin secrets
|
||||
vault-fetch hosts/monitoring01/grafana-admin /run/secrets/grafana /var/lib/vault/cache/grafana
|
||||
|
||||
# Use default cache location
|
||||
vault-fetch hosts/monitoring01/grafana-admin /run/secrets/grafana
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Read Credentials**: Loads `role_id` and `secret_id` from `/var/lib/vault/approle/`
|
||||
2. **Authenticate**: Calls `POST /v1/auth/approle/login` to get a Vault token
|
||||
3. **Fetch Secret**: Retrieves secret from `GET /v1/secret/data/{path}`
|
||||
4. **Extract Keys**: Parses JSON response and extracts individual secret keys
|
||||
5. **Write Files**: Creates one file per secret key in output directory
|
||||
6. **Update Cache**: Copies secrets to cache directory for fallback
|
||||
7. **Set Permissions**: Ensures all files have 600 permissions (owner read/write only)
|
||||
|
||||
## Error Handling
|
||||
|
||||
If Vault is unreachable or authentication fails:
|
||||
- Script logs a warning to stderr
|
||||
- Falls back to cached secrets from previous successful fetch
|
||||
- Exits with error code 1 if no cache is available
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `VAULT_ADDR`: Vault server address (default: `https://vault01.home.2rjus.net:8200`)
|
||||
- `VAULT_SKIP_VERIFY`: Skip TLS verification (default: `1`)
|
||||
|
||||
## Integration with NixOS
|
||||
|
||||
This tool is designed to be called from systemd service `ExecStartPre` hooks via the `vault.secrets` NixOS module:
|
||||
|
||||
```nix
|
||||
vault.secrets.grafana-admin = {
|
||||
secretPath = "hosts/monitoring01/grafana-admin";
|
||||
};
|
||||
|
||||
# Service automatically gets secrets fetched before start
|
||||
systemd.services.grafana.serviceConfig = {
|
||||
EnvironmentFile = "/run/secrets/grafana-admin/password";
|
||||
};
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- `curl`: For Vault API calls
|
||||
- `jq`: For JSON parsing
|
||||
- `coreutils`: For file operations
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- AppRole credentials stored at `/var/lib/vault/approle/` should be root-owned with 600 permissions
|
||||
- Tokens are ephemeral and not stored - fresh authentication on each fetch
|
||||
- Secrets written to tmpfs (`/run/secrets/`) are lost on reboot
|
||||
- Cache directory persists across reboots for service availability
|
||||
- All secret files have restrictive permissions (600)
|
||||
18
scripts/vault-fetch/default.nix
Normal file
18
scripts/vault-fetch/default.nix
Normal file
@@ -0,0 +1,18 @@
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
pkgs.writeShellApplication {
|
||||
name = "vault-fetch";
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
curl # Vault API calls
|
||||
jq # JSON parsing
|
||||
coreutils # File operations
|
||||
];
|
||||
|
||||
text = builtins.readFile ./vault-fetch.sh;
|
||||
|
||||
meta = with lib; {
|
||||
description = "Fetch secrets from OpenBao/Vault and write to filesystem";
|
||||
license = licenses.mit;
|
||||
};
|
||||
}
|
||||
152
scripts/vault-fetch/vault-fetch.sh
Normal file
152
scripts/vault-fetch/vault-fetch.sh
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# vault-fetch: Fetch secrets from OpenBao/Vault and write to filesystem
|
||||
#
|
||||
# Usage: vault-fetch <secret-path> <output-directory> [cache-directory]
|
||||
#
|
||||
# Example: vault-fetch hosts/monitoring01/grafana-admin /run/secrets/grafana /var/lib/vault/cache/grafana
|
||||
#
|
||||
# This script:
|
||||
# 1. Authenticates to Vault using AppRole credentials from /var/lib/vault/approle/
|
||||
# 2. Fetches secrets from the specified path
|
||||
# 3. Writes each secret key as an individual file in the output directory
|
||||
# 4. Updates cache for fallback when Vault is unreachable
|
||||
# 5. Falls back to cache if Vault authentication fails or is unreachable
|
||||
|
||||
# Parse arguments
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: vault-fetch <secret-path> <output-directory> [cache-directory]" >&2
|
||||
echo "Example: vault-fetch hosts/monitoring01/grafana /run/secrets/grafana /var/lib/vault/cache/grafana" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SECRET_PATH="$1"
|
||||
OUTPUT_DIR="$2"
|
||||
CACHE_DIR="${3:-/var/lib/vault/cache/$(basename "$OUTPUT_DIR")}"
|
||||
|
||||
# Vault configuration
|
||||
VAULT_ADDR="${VAULT_ADDR:-https://vault01.home.2rjus.net:8200}"
|
||||
VAULT_SKIP_VERIFY="${VAULT_SKIP_VERIFY:-1}"
|
||||
APPROLE_DIR="/var/lib/vault/approle"
|
||||
|
||||
# TLS verification flag for curl
|
||||
if [ "$VAULT_SKIP_VERIFY" = "1" ]; then
|
||||
CURL_TLS_FLAG="-k"
|
||||
else
|
||||
CURL_TLS_FLAG=""
|
||||
fi
|
||||
|
||||
# Logging helper
|
||||
log() {
|
||||
echo "[vault-fetch] $*" >&2
|
||||
}
|
||||
|
||||
# Error handler
|
||||
error() {
|
||||
log "ERROR: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if cache is available
|
||||
has_cache() {
|
||||
[ -d "$CACHE_DIR" ] && [ -n "$(ls -A "$CACHE_DIR" 2>/dev/null)" ]
|
||||
}
|
||||
|
||||
# Use cached secrets
|
||||
use_cache() {
|
||||
if ! has_cache; then
|
||||
error "No cache available and Vault is unreachable"
|
||||
fi
|
||||
|
||||
log "WARNING: Using cached secrets from $CACHE_DIR"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
cp -r "$CACHE_DIR"/* "$OUTPUT_DIR/"
|
||||
chmod -R u=rw,go= "$OUTPUT_DIR"/*
|
||||
}
|
||||
|
||||
# Fetch secrets from Vault
|
||||
fetch_from_vault() {
|
||||
# Read AppRole credentials
|
||||
if [ ! -f "$APPROLE_DIR/role-id" ] || [ ! -f "$APPROLE_DIR/secret-id" ]; then
|
||||
log "WARNING: AppRole credentials not found at $APPROLE_DIR"
|
||||
use_cache
|
||||
return
|
||||
fi
|
||||
|
||||
ROLE_ID=$(cat "$APPROLE_DIR/role-id")
|
||||
SECRET_ID=$(cat "$APPROLE_DIR/secret-id")
|
||||
|
||||
# Authenticate to Vault
|
||||
log "Authenticating to Vault at $VAULT_ADDR"
|
||||
AUTH_RESPONSE=$(curl -s $CURL_TLS_FLAG -X POST \
|
||||
-d "{\"role_id\":\"$ROLE_ID\",\"secret_id\":\"$SECRET_ID\"}" \
|
||||
"$VAULT_ADDR/v1/auth/approle/login" 2>&1) || {
|
||||
log "WARNING: Failed to connect to Vault"
|
||||
use_cache
|
||||
return
|
||||
}
|
||||
|
||||
# Check for errors in response
|
||||
if echo "$AUTH_RESPONSE" | jq -e '.errors' >/dev/null 2>&1; then
|
||||
ERRORS=$(echo "$AUTH_RESPONSE" | jq -r '.errors[]' 2>/dev/null || echo "Unknown error")
|
||||
log "WARNING: Vault authentication failed: $ERRORS"
|
||||
use_cache
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract token
|
||||
VAULT_TOKEN=$(echo "$AUTH_RESPONSE" | jq -r '.auth.client_token' 2>/dev/null)
|
||||
if [ -z "$VAULT_TOKEN" ] || [ "$VAULT_TOKEN" = "null" ]; then
|
||||
log "WARNING: Failed to extract Vault token from response"
|
||||
use_cache
|
||||
return
|
||||
fi
|
||||
|
||||
log "Successfully authenticated to Vault"
|
||||
|
||||
# Fetch secret
|
||||
log "Fetching secret from path: $SECRET_PATH"
|
||||
SECRET_RESPONSE=$(curl -s $CURL_TLS_FLAG \
|
||||
-H "X-Vault-Token: $VAULT_TOKEN" \
|
||||
"$VAULT_ADDR/v1/secret/data/$SECRET_PATH" 2>&1) || {
|
||||
log "WARNING: Failed to fetch secret from Vault"
|
||||
use_cache
|
||||
return
|
||||
}
|
||||
|
||||
# Check for errors
|
||||
if echo "$SECRET_RESPONSE" | jq -e '.errors' >/dev/null 2>&1; then
|
||||
ERRORS=$(echo "$SECRET_RESPONSE" | jq -r '.errors[]' 2>/dev/null || echo "Unknown error")
|
||||
log "WARNING: Failed to fetch secret: $ERRORS"
|
||||
use_cache
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract secret data
|
||||
SECRET_DATA=$(echo "$SECRET_RESPONSE" | jq -r '.data.data' 2>/dev/null)
|
||||
if [ -z "$SECRET_DATA" ] || [ "$SECRET_DATA" = "null" ]; then
|
||||
log "WARNING: No secret data found at path $SECRET_PATH"
|
||||
use_cache
|
||||
return
|
||||
fi
|
||||
|
||||
# Create output and cache directories
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
# Write each secret key to a separate file
|
||||
log "Writing secrets to $OUTPUT_DIR"
|
||||
echo "$SECRET_DATA" | jq -r 'to_entries[] | "\(.key)\n\(.value)"' | while read -r key; read -r value; do
|
||||
echo -n "$value" > "$OUTPUT_DIR/$key"
|
||||
echo -n "$value" > "$CACHE_DIR/$key"
|
||||
chmod 600 "$OUTPUT_DIR/$key"
|
||||
chmod 600 "$CACHE_DIR/$key"
|
||||
log " - Wrote secret key: $key"
|
||||
done
|
||||
|
||||
log "Successfully fetched and cached secrets"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
fetch_from_vault
|
||||
Reference in New Issue
Block a user