#!/bin/bash # Vault Snapshot Backup to Cloudflare R2 # Usage: ./vault-backup-r2.sh [snapshot-name] set -euo pipefail # ============================================ # Configuration # ============================================ VAULT_ADDR="${VAULT_ADDR:-https://vault.anvil.it.com}" VAULT_TOKEN="${VAULT_TOKEN:?VAULT_TOKEN is required}" # R2 Configuration R2_ACCOUNT_ID="${R2_ACCOUNT_ID:?R2_ACCOUNT_ID is required}" R2_ACCESS_KEY="${R2_ACCESS_KEY:?R2_ACCESS_KEY is required}" R2_SECRET_KEY="${R2_SECRET_KEY:?R2_SECRET_KEY is required}" R2_BUCKET="${R2_BUCKET:-vault-backup}" R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" # Backup settings BACKUP_DIR="${BACKUP_DIR:-/tmp/vault-backups}" RETENTION_DAYS="${RETENTION_DAYS:-30}" TIMESTAMP=$(date +%Y%m%d-%H%M%S) SNAPSHOT_NAME="${1:-vault-snapshot-${TIMESTAMP}}" # ============================================ # Functions # ============================================ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } error() { log "ERROR: $*" >&2 exit 1 } check_dependencies() { for cmd in curl aws jq; do if ! command -v "$cmd" &>/dev/null; then error "$cmd is required but not installed" fi done } create_snapshot() { log "Creating Vault snapshot..." mkdir -p "$BACKUP_DIR" local snapshot_file="${BACKUP_DIR}/${SNAPSHOT_NAME}.snap" # Vault Raft snapshot (requires Raft storage backend) curl -s -X POST \ -H "X-Vault-Token: ${VAULT_TOKEN}" \ "${VAULT_ADDR}/v1/sys/storage/raft/snapshot" \ -o "$snapshot_file" \ --fail || { # Fallback: Export secrets as JSON (for non-Raft backends) log "Raft snapshot failed, trying secrets export..." export_secrets "$snapshot_file" } echo "$snapshot_file" } export_secrets() { local output_file="$1" local secrets_file="${output_file%.snap}.json" log "Exporting secrets to JSON..." # Get list of secret paths (requires list permission) local paths=$(curl -s -X LIST \ -H "X-Vault-Token: ${VAULT_TOKEN}" \ "${VAULT_ADDR}/v1/secret/metadata" | jq -r '.data.keys[]?' 2>/dev/null || echo "") if [[ -z "$paths" ]]; then log "No secrets found or no list permission" echo '{"secrets": [], "timestamp": "'$(date -Iseconds)'", "note": "empty or no permission"}' > "$secrets_file" else echo '{"secrets": [' > "$secrets_file" local first=true for path in $paths; do local secret=$(curl -s \ -H "X-Vault-Token: ${VAULT_TOKEN}" \ "${VAULT_ADDR}/v1/secret/data/${path}" | jq '.data' 2>/dev/null) if [[ -n "$secret" && "$secret" != "null" ]]; then if [[ "$first" == "true" ]]; then first=false else echo "," >> "$secrets_file" fi echo "{\"path\": \"${path}\", \"data\": ${secret}}" >> "$secrets_file" fi done echo '], "timestamp": "'$(date -Iseconds)'"}' >> "$secrets_file" fi # Compress and encrypt (optional) gzip -c "$secrets_file" > "$output_file" rm -f "$secrets_file" } upload_to_r2() { local file="$1" local filename=$(basename "$file") log "Uploading to R2: ${R2_BUCKET}/${filename}" # Configure AWS CLI for R2 export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY" export AWS_SECRET_ACCESS_KEY="$R2_SECRET_KEY" aws s3 cp "$file" "s3://${R2_BUCKET}/${filename}" \ --endpoint-url "$R2_ENDPOINT" \ --quiet log "Upload complete: s3://${R2_BUCKET}/${filename}" } cleanup_old_backups() { log "Cleaning up backups older than ${RETENTION_DAYS} days..." export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY" export AWS_SECRET_ACCESS_KEY="$R2_SECRET_KEY" local cutoff_date=$(date -v-${RETENTION_DAYS}d +%Y-%m-%d 2>/dev/null || date -d "${RETENTION_DAYS} days ago" +%Y-%m-%d) aws s3 ls "s3://${R2_BUCKET}/" --endpoint-url "$R2_ENDPOINT" 2>/dev/null | \ while read -r line; do local file_date=$(echo "$line" | awk '{print $1}') local file_name=$(echo "$line" | awk '{print $4}') if [[ "$file_date" < "$cutoff_date" && -n "$file_name" ]]; then log "Deleting old backup: $file_name" aws s3 rm "s3://${R2_BUCKET}/${file_name}" --endpoint-url "$R2_ENDPOINT" --quiet fi done } cleanup_local() { log "Cleaning up local backup files..." find "$BACKUP_DIR" -name "vault-snapshot-*.snap" -mtime +1 -delete 2>/dev/null || true find "$BACKUP_DIR" -name "vault-snapshot-*.gz" -mtime +1 -delete 2>/dev/null || true } # ============================================ # Main # ============================================ main() { log "=== Vault Backup to R2 Started ===" log "Vault: $VAULT_ADDR" log "R2 Bucket: $R2_BUCKET" check_dependencies local snapshot_file snapshot_file=$(create_snapshot) if [[ -f "$snapshot_file" ]]; then upload_to_r2 "$snapshot_file" cleanup_old_backups cleanup_local log "=== Backup Complete ===" else error "Snapshot file not created" fi } main "$@"