provisioning/scripts/secrets-rotate-keys.nu

163 lines
4.2 KiB
Text
Raw Normal View History

#!/usr/bin/env nu
# Rotate Age keys and re-encrypt all SOPS files
# Generates new Age keypair in vault-service, then re-encrypts all SOPS files
# Usage: secrets-rotate-keys [--environment dev|staging|prod] [--pattern <glob>]
use std log
def get-vault-url [] {
$env.VAULT_SERVICE_URL? // "http://localhost:9094"
}
def get-vault-token [] {
$env.VAULT_SERVICE_TOKEN? // ""
}
def rotate-keypair-in-vault [environment: string] {
let url = $"(get-vault-url)/api/v1/age/rotate?env=($environment)"
let token = (get-vault-token)
if ($token | is-empty) {
error make {
msg: "VAULT_SERVICE_TOKEN required for key rotation"
label: {
text: "Set environment variable"
span: (metadata $environment).span
}
}
}
print "🔑 Rotating Age keypair in vault-service..."
let response = (http post -H {"X-Vault-Token": $token} $url {} | complete)
if $response.exit_code != 0 {
error make {
msg: "Failed to rotate keypair"
label: {
text: "vault-service rejected request"
span: (metadata $environment).span
}
}
}
let json = ($response.stdout | from json)
print $"✓ Rotated: version ($json.previous_version) → ($json.new_version)"
$json
}
def find-sops-files [environment: string, pattern: string] {
let glob_pattern = if ($pattern | is-empty) {
$"config/secrets/($environment)/**/*.yaml"
} else {
$pattern
}
let files = (glob $glob_pattern | sort)
if ($files | is-empty) {
print $"⚠️ No SOPS files found for pattern: ($glob_pattern)"
}
$files
}
def update-sops-file [file: string] {
print $" Re-encrypting: ($file)..."
let result = (
^sops updatekeys --yes $file
| complete
)
if $result.exit_code != 0 {
let stderr = $result.stderr
print $" ❌ Failed: ($stderr)"
false
} else {
print $" ✓ Re-encrypted"
true
}
}
def main [
--environment: string = "dev"
--pattern: string = ""
] {
# Validate environment
if $environment not-in ["dev", "staging", "prod"] {
error make {
msg: "Invalid environment"
label: {
text: "Must be: dev, staging, or prod"
span: (metadata $environment).span
}
}
}
print $"====== Age Key Rotation for ($environment) ======"
print ""
# Step 1: Rotate keypair
let rotation = (rotate-keypair-in-vault $environment)
print ""
print "🔄 Re-encrypting SOPS files with new public key..."
# Step 2: Find SOPS files
let sops_files = (find-sops-files $environment $pattern)
if ($sops_files | is-empty) {
print "⚠️ No SOPS files to re-encrypt"
return {
success: true
rotated: true
files_updated: 0
message: "Key rotated but no files found to re-encrypt"
}
}
print $"Found ($sops_files | length) files to re-encrypt"
print ""
# Step 3: Re-encrypt each file
let success_count = (
$sops_files | map {|file|
update-sops-file $file
} | where {|result| $result} | length
)
let fail_count = (
$sops_files | map {|file|
update-sops-file $file
} | where {|result| not $result} | length
)
print ""
print "====== Rotation Complete ======"
print $"✓ Success: ($success_count) files"
if ($fail_count > 0) {
print $"❌ Failed: ($fail_count) files"
}
print ""
if ($fail_count > 0) {
error make {
msg: "Some files failed to re-encrypt"
label: {
text: "Review errors above and retry manually"
span: (metadata $environment).span
}
}
}
{
success: true
environment: $environment
rotated: true
previous_version: $rotation.previous_version
new_version: $rotation.new_version
files_updated: $success_count
files_failed: $fail_count
next_steps: $"Verify decryption: for f in config/secrets/($environment)/*.yaml; do sops --decrypt \\$f | head -1; done"
}
}