395 lines
13 KiB
Plaintext
395 lines
13 KiB
Plaintext
|
|
# Configuration Encryption CLI Commands
|
|||
|
|
# Provides user-friendly commands for config encryption operations
|
|||
|
|
|
|||
|
|
use encryption.nu *
|
|||
|
|
use accessor.nu *
|
|||
|
|
|
|||
|
|
# Encrypt a configuration file
|
|||
|
|
export def "config encrypt" [
|
|||
|
|
file: path # Configuration file to encrypt
|
|||
|
|
--output (-o): path # Output path (default: <file>.enc)
|
|||
|
|
--kms (-k): string = "age" # KMS backend: age, aws-kms, vault, cosmian
|
|||
|
|
--in-place (-i) # Encrypt in-place (overwrites original)
|
|||
|
|
--debug (-d) # Enable debug output
|
|||
|
|
] {
|
|||
|
|
if not ($file | path exists) {
|
|||
|
|
print $"❌ File not found: ($file)"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $"🔒 Encrypting configuration file: ($file)"
|
|||
|
|
print $" Backend: ($kms)"
|
|||
|
|
|
|||
|
|
if $in_place {
|
|||
|
|
print $" Mode: In-place (will overwrite original)"
|
|||
|
|
} else if ($output | is-not-empty) {
|
|||
|
|
print $" Output: ($output)"
|
|||
|
|
} else {
|
|||
|
|
print $" Output: ($file).enc"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if $in_place {
|
|||
|
|
encrypt-config $file --kms=$kms --in-place --debug=$debug
|
|||
|
|
} else {
|
|||
|
|
encrypt-config $file $output --kms=$kms --debug=$debug
|
|||
|
|
}
|
|||
|
|
} catch { |err|
|
|||
|
|
print $"❌ Encryption failed: ($err.msg)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Decrypt a configuration file
|
|||
|
|
export def "config decrypt" [
|
|||
|
|
file: path # Encrypted configuration file
|
|||
|
|
--output (-o): path # Output path (default: removes .enc extension)
|
|||
|
|
--in-place (-i) # Decrypt in-place (overwrites original)
|
|||
|
|
--debug (-d) # Enable debug output
|
|||
|
|
] {
|
|||
|
|
if not ($file | path exists) {
|
|||
|
|
print $"❌ File not found: ($file)"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $"🔓 Decrypting configuration file: ($file)"
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if $in_place {
|
|||
|
|
decrypt-config $file --in-place --debug=$debug
|
|||
|
|
} else {
|
|||
|
|
decrypt-config $file $output --debug=$debug
|
|||
|
|
}
|
|||
|
|
} catch { |err|
|
|||
|
|
print $"❌ Decryption failed: ($err.msg)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Edit encrypted configuration file securely
|
|||
|
|
export def "config edit-secure" [
|
|||
|
|
file: path # Encrypted configuration file
|
|||
|
|
--editor (-e): string # Editor to use (default: $EDITOR or vim)
|
|||
|
|
--debug (-d) # Enable debug output
|
|||
|
|
] {
|
|||
|
|
if not ($file | path exists) {
|
|||
|
|
print $"❌ File not found: ($file)"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if ($editor | is-not-empty) {
|
|||
|
|
edit-encrypted-config $file --editor=$editor --debug=$debug
|
|||
|
|
} else {
|
|||
|
|
edit-encrypted-config $file --debug=$debug
|
|||
|
|
}
|
|||
|
|
} catch { |err|
|
|||
|
|
print $"❌ Edit failed: ($err.msg)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Rotate encryption keys for a configuration file
|
|||
|
|
export def "config rotate-keys" [
|
|||
|
|
file: path # Encrypted configuration file
|
|||
|
|
new_key: string # New key ID or recipient
|
|||
|
|
--debug (-d) # Enable debug output
|
|||
|
|
] {
|
|||
|
|
if not ($file | path exists) {
|
|||
|
|
print $"❌ File not found: ($file)"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $"🔄 Rotating encryption keys"
|
|||
|
|
print $" File: ($file)"
|
|||
|
|
print $" New key: ($new_key)"
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
rotate-encryption-keys $file $new_key --debug=$debug
|
|||
|
|
} catch { |err|
|
|||
|
|
print $"❌ Key rotation failed: ($err.msg)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check if a configuration file is encrypted
|
|||
|
|
export def "config is-encrypted" [
|
|||
|
|
file: path # Configuration file to check
|
|||
|
|
] {
|
|||
|
|
if not ($file | path exists) {
|
|||
|
|
print $"❌ File not found: ($file)"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let encrypted = (is-encrypted-config $file)
|
|||
|
|
|
|||
|
|
if $encrypted {
|
|||
|
|
print $"🔒 File is encrypted: ($file)"
|
|||
|
|
} else {
|
|||
|
|
print $"🔓 File is not encrypted: ($file)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$encrypted
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Validate encryption configuration and setup
|
|||
|
|
export def "config validate-encryption" [] {
|
|||
|
|
print $"🔍 Validating encryption configuration..."
|
|||
|
|
print ""
|
|||
|
|
|
|||
|
|
let validation = (validate-encryption-config)
|
|||
|
|
|
|||
|
|
# Show summary
|
|||
|
|
print $"📊 Encryption Configuration Summary"
|
|||
|
|
print $"=================================="
|
|||
|
|
print $" SOPS installed: ($validation.summary.sops_installed)"
|
|||
|
|
print $" Age backend: ($validation.summary.age_backend)"
|
|||
|
|
print $" KMS enabled: ($validation.summary.kms_enabled)"
|
|||
|
|
print $" Errors: ($validation.summary.total_errors)"
|
|||
|
|
print $" Warnings: ($validation.summary.total_warnings)"
|
|||
|
|
print ""
|
|||
|
|
|
|||
|
|
# Show errors
|
|||
|
|
if ($validation.errors | length) > 0 {
|
|||
|
|
print $"❌ Errors:"
|
|||
|
|
for error in $validation.errors {
|
|||
|
|
print $" • ($error.message)"
|
|||
|
|
}
|
|||
|
|
print ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Show warnings
|
|||
|
|
if ($validation.warnings | length) > 0 {
|
|||
|
|
print $"⚠️ Warnings:"
|
|||
|
|
for warning in $validation.warnings {
|
|||
|
|
print $" • ($warning.message)"
|
|||
|
|
}
|
|||
|
|
print ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if $validation.valid {
|
|||
|
|
print $"✅ Encryption configuration is valid"
|
|||
|
|
} else {
|
|||
|
|
print $"❌ Encryption configuration has errors"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$validation
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Scan directory for unencrypted sensitive configurations
|
|||
|
|
export def "config scan-sensitive" [
|
|||
|
|
directory: path = "." # Directory to scan
|
|||
|
|
--recursive (-r) # Scan recursively
|
|||
|
|
] {
|
|||
|
|
if not ($directory | path exists) {
|
|||
|
|
print $"❌ Directory not found: ($directory)"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $"🔍 Scanning for unencrypted sensitive configs in: ($directory)"
|
|||
|
|
|
|||
|
|
let results = (scan-unencrypted-configs $directory --recursive=$recursive)
|
|||
|
|
|
|||
|
|
if ($results | is-empty) {
|
|||
|
|
print $"✅ No unencrypted sensitive configs found"
|
|||
|
|
} else {
|
|||
|
|
print $"\n⚠️ Found ($results | length) unencrypted sensitive configs:"
|
|||
|
|
print ""
|
|||
|
|
print $results
|
|||
|
|
print ""
|
|||
|
|
print $"💡 Run 'config encrypt-all ($directory)' to encrypt them"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$results
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Encrypt all sensitive configurations in directory
|
|||
|
|
export def "config encrypt-all" [
|
|||
|
|
directory: path = "." # Directory to encrypt
|
|||
|
|
--kms (-k): string = "age" # KMS backend: age, aws-kms, vault, cosmian
|
|||
|
|
--recursive (-r) # Scan recursively
|
|||
|
|
--dry-run (-n) # Dry run (no actual encryption)
|
|||
|
|
] {
|
|||
|
|
if not ($directory | path exists) {
|
|||
|
|
print $"❌ Directory not found: ($directory)"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
encrypt-sensitive-configs $directory --kms=$kms --recursive=$recursive --dry-run=$dry_run
|
|||
|
|
} catch { |err|
|
|||
|
|
print $"❌ Bulk encryption failed: ($err.msg)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Show encryption information for configuration
|
|||
|
|
export def "config encryption-info" [
|
|||
|
|
file: path # Configuration file
|
|||
|
|
] {
|
|||
|
|
if not ($file | path exists) {
|
|||
|
|
print $"❌ File not found: ($file)"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $"📋 Encryption Information"
|
|||
|
|
print $"========================"
|
|||
|
|
print $" File: ($file)"
|
|||
|
|
|
|||
|
|
let encrypted = (is-encrypted-config $file)
|
|||
|
|
print $" Encrypted: ($encrypted)"
|
|||
|
|
|
|||
|
|
if $encrypted {
|
|||
|
|
# Try to extract SOPS metadata
|
|||
|
|
try {
|
|||
|
|
let content = (open $file --raw)
|
|||
|
|
if ($content | str contains "sops:") {
|
|||
|
|
print $" Type: SOPS encrypted"
|
|||
|
|
|
|||
|
|
# Extract some metadata (without decrypting)
|
|||
|
|
if ($content | str contains "age:") {
|
|||
|
|
print $" Backend: Age"
|
|||
|
|
}
|
|||
|
|
if ($content | str contains "kms:") {
|
|||
|
|
print $" Backend: AWS KMS"
|
|||
|
|
}
|
|||
|
|
if ($content | str contains "vault:") {
|
|||
|
|
print $" Backend: Vault"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
print $" Type: Encrypted (unknown format)"
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
let sensitive = (contains-sensitive-data $file)
|
|||
|
|
print $" Contains sensitive data: ($sensitive)"
|
|||
|
|
|
|||
|
|
if $sensitive {
|
|||
|
|
print ""
|
|||
|
|
print $"⚠️ This file contains sensitive data but is not encrypted!"
|
|||
|
|
print $"💡 Run 'config encrypt ($file)' to encrypt it"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Initialize encryption setup (generate keys, create SOPS config)
|
|||
|
|
export def "config init-encryption" [
|
|||
|
|
--kms (-k): string = "age" # KMS backend to initialize: age, aws-kms, vault
|
|||
|
|
--force (-f) # Force re-initialization
|
|||
|
|
] {
|
|||
|
|
print $"🔧 Initializing encryption setup with ($kms)"
|
|||
|
|
print ""
|
|||
|
|
|
|||
|
|
match $kms {
|
|||
|
|
"age" => {
|
|||
|
|
# Check if age is installed
|
|||
|
|
let age_check = (^which age | complete)
|
|||
|
|
if $age_check.exit_code != 0 {
|
|||
|
|
print $"❌ Age is not installed"
|
|||
|
|
print $"💡 Install with: brew install age"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate age key if not exists
|
|||
|
|
let age_key_file = ($env.HOME | path join ".config" | path join "sops" | path join "age" | path join "keys.txt")
|
|||
|
|
let age_key_dir = ($age_key_file | path dirname)
|
|||
|
|
|
|||
|
|
if ($age_key_file | path exists) and not $force {
|
|||
|
|
print $"✅ Age key already exists: ($age_key_file)"
|
|||
|
|
print $" Use --force to regenerate"
|
|||
|
|
} else {
|
|||
|
|
# Create directory
|
|||
|
|
mkdir $age_key_dir
|
|||
|
|
|
|||
|
|
# Generate new age key
|
|||
|
|
print $"🔑 Generating new Age key..."
|
|||
|
|
let key_output = (^age-keygen -o $age_key_file | complete)
|
|||
|
|
|
|||
|
|
if $key_output.exit_code == 0 {
|
|||
|
|
print $"✅ Age key generated: ($age_key_file)"
|
|||
|
|
|
|||
|
|
# Extract recipient
|
|||
|
|
let key_content = (open $age_key_file --raw)
|
|||
|
|
let recipient = ($key_content | lines | where ($it | str starts-with "# public key:") | first | split row ": " | get 1)
|
|||
|
|
|
|||
|
|
print $" Public key (recipient): ($recipient)"
|
|||
|
|
print ""
|
|||
|
|
print $"💡 Set this environment variable:"
|
|||
|
|
print $" export SOPS_AGE_RECIPIENTS=($recipient)"
|
|||
|
|
} else {
|
|||
|
|
print $"❌ Failed to generate Age key"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Create .sops.yaml if not exists
|
|||
|
|
let sops_config = ($env.PWD | path join ".sops.yaml")
|
|||
|
|
if not ($sops_config | path exists) or $force {
|
|||
|
|
print $"📝 Creating SOPS configuration: ($sops_config)"
|
|||
|
|
|
|||
|
|
let key_content = (open $age_key_file --raw)
|
|||
|
|
let recipient = ($key_content | lines | where ($it | str starts-with "# public key:") | first | split row ": " | get 1)
|
|||
|
|
|
|||
|
|
let sops_yaml = $"creation_rules:
|
|||
|
|
- path_regex: .*\\.enc\\.yaml$
|
|||
|
|
age: ($recipient)
|
|||
|
|
- path_regex: .*\\.enc\\.yml$
|
|||
|
|
age: ($recipient)
|
|||
|
|
- path_regex: .*\\.enc\\.toml$
|
|||
|
|
age: ($recipient)
|
|||
|
|
- path_regex: .*\\.enc\\.json$
|
|||
|
|
age: ($recipient)
|
|||
|
|
- path_regex: workspace/.*/config/secure\\.yaml$
|
|||
|
|
age: ($recipient)
|
|||
|
|
"
|
|||
|
|
|
|||
|
|
$sops_yaml | save --force $sops_config
|
|||
|
|
print $"✅ SOPS configuration created"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
"aws-kms" => {
|
|||
|
|
print $"⚠️ AWS KMS requires manual configuration"
|
|||
|
|
print $"💡 Follow these steps:"
|
|||
|
|
print $" 1. Create KMS key in AWS Console"
|
|||
|
|
print $" 2. Update .sops.yaml with KMS ARN"
|
|||
|
|
print $" 3. Configure AWS credentials"
|
|||
|
|
}
|
|||
|
|
"vault" | "cosmian" => {
|
|||
|
|
print $"⚠️ ($kms) KMS requires manual configuration"
|
|||
|
|
print $"💡 Configure KMS settings in config file"
|
|||
|
|
}
|
|||
|
|
_ => {
|
|||
|
|
print $"❌ Unknown KMS backend: ($kms)"
|
|||
|
|
print $" Supported: age, aws-kms, vault, cosmian"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print ""
|
|||
|
|
print $"✅ Encryption initialization completed"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Main help command
|
|||
|
|
export def main [] {
|
|||
|
|
print "Configuration Encryption Commands"
|
|||
|
|
print "================================="
|
|||
|
|
print ""
|
|||
|
|
print "Encryption/Decryption:"
|
|||
|
|
print " config encrypt <file> Encrypt configuration file"
|
|||
|
|
print " config decrypt <file> Decrypt configuration file"
|
|||
|
|
print " config edit-secure <file> Edit encrypted file securely"
|
|||
|
|
print " config rotate-keys <file> Rotate encryption keys"
|
|||
|
|
print ""
|
|||
|
|
print "Information:"
|
|||
|
|
print " config is-encrypted <file> Check if file is encrypted"
|
|||
|
|
print " config encryption-info <file> Show encryption details"
|
|||
|
|
print " config validate-encryption Validate encryption setup"
|
|||
|
|
print ""
|
|||
|
|
print "Bulk Operations:"
|
|||
|
|
print " config scan-sensitive <dir> Find unencrypted sensitive configs"
|
|||
|
|
print " config encrypt-all <dir> Encrypt all sensitive configs"
|
|||
|
|
print ""
|
|||
|
|
print "Setup:"
|
|||
|
|
print " config init-encryption Initialize encryption (generate keys)"
|
|||
|
|
print ""
|
|||
|
|
print "Examples:"
|
|||
|
|
print " config encrypt workspace/config/secure.yaml"
|
|||
|
|
print " config edit-secure workspace/config/secure.enc.yaml"
|
|||
|
|
print " config scan-sensitive workspace/config --recursive"
|
|||
|
|
print " config init-encryption --kms age"
|
|||
|
|
}
|