# Configuration Encryption Module for Provisioning System # Provides transparent encryption/decryption for configuration files using SOPS use std log use ../sops/lib.nu * use ../kms/lib.nu * use accessor.nu * # Detect if a config file is encrypted export def is-encrypted-config [ file_path: string ]: nothing -> bool { if not ($file_path | path exists) { return false } # Check if file has SOPS metadata is_sops_file $file_path } # Load configuration file with automatic decryption export def load-encrypted-config [ file_path: string --debug = false ]: nothing -> record { if not ($file_path | path exists) { error make { msg: $"Configuration file not found: ($file_path)" } } if $debug { print $"Loading configuration file: ($file_path)" } # Check if file is encrypted if (is-encrypted-config $file_path) { if $debug { print $" Detected encrypted file, decrypting..." } # Decrypt in memory (never write to disk) let decrypted_content = (decrypt-config-memory $file_path --debug=$debug) # Parse based on file extension let ext = ($file_path | path parse | get extension) match $ext { "yaml" | "yml" => ($decrypted_content | from yaml) "toml" => ($decrypted_content | from toml) "json" => ($decrypted_content | from json) _ => { error make { msg: $"Unsupported encrypted config format: ($ext)" } } } } else { if $debug { print $" Loading unencrypted file..." } # Load unencrypted file normally open $file_path } } # Decrypt configuration file in memory only (never writes to disk) export def decrypt-config-memory [ file_path: string --debug = false ]: nothing -> string { if not (is-encrypted-config $file_path) { error make { msg: $"File is not encrypted: ($file_path)" } } # Use SOPS to decrypt (output goes to stdout, captured in memory) let decrypted = (on_sops "decrypt" $file_path --quiet) if ($decrypted | is-empty) { error make { msg: $"Failed to decrypt configuration file: ($file_path)" } } $decrypted } # Encrypt a configuration file export def encrypt-config [ source_path: string output_path?: string --kms: string = "age" # age, aws-kms, vault --in-place = false --debug = false ]: nothing -> nothing { if not ($source_path | path exists) { error make { msg: $"Source file not found: ($source_path)" } } # Check if already encrypted if (is-encrypted-config $source_path) { print $"⚠️ File is already encrypted: ($source_path)" return } # Determine output path let target = if $in_place { $source_path } else if ($output_path | is-not-empty) { $output_path } else { $"($source_path).enc" } if $debug { print $"Encrypting ($source_path) → ($target) using ($kms)" } # Encrypt based on KMS backend match $kms { "age" => { let encrypted = (on_sops "encrypt" $source_path) if ($encrypted | is-empty) { error make { msg: $"Failed to encrypt file with age: ($source_path)" } } $encrypted | save --force $target print $"✅ Encrypted successfully: ($target)" } "aws-kms" => { # For AWS KMS, SOPS will use AWS KMS backend if configured let encrypted = (on_sops "encrypt" $source_path) if ($encrypted | is-empty) { error make { msg: $"Failed to encrypt file with AWS KMS: ($source_path)" } } $encrypted | save --force $target print $"✅ Encrypted successfully with AWS KMS: ($target)" } "vault" | "cosmian" => { # Use KMS client for Vault or Cosmian KMS let encrypted = (on_kms "encrypt" $source_path) if ($encrypted | is-empty) { error make { msg: $"Failed to encrypt file with ($kms): ($source_path)" } } $encrypted | save --force $target print $"✅ Encrypted successfully with ($kms): ($target)" } _ => { error make { msg: $"Unsupported KMS backend: ($kms). Supported: age, aws-kms, vault, cosmian" } } } } # Decrypt a configuration file export def decrypt-config [ source_path: string output_path?: string --in-place = false --debug = false ]: nothing -> nothing { if not ($source_path | path exists) { error make { msg: $"Source file not found: ($source_path)" } } # Check if encrypted if not (is-encrypted-config $source_path) { print $"⚠️ File is not encrypted: ($source_path)" return } # Determine output path let target = if $in_place { $source_path } else if ($output_path | is-not-empty) { $output_path } else { # Remove .enc extension if present if ($source_path | str ends-with ".enc") { $source_path | str substring 0..-5 } else { $"($source_path).dec" } } if $debug { print $"Decrypting ($source_path) → ($target)" } # Decrypt using SOPS (works with any backend) let decrypted = (on_sops "decrypt" $source_path) if ($decrypted | is-empty) { error make { msg: $"Failed to decrypt file: ($source_path)" } } $decrypted | save --force $target print $"✅ Decrypted successfully: ($target)" } # Edit encrypted config file (decrypt, edit, re-encrypt) export def edit-encrypted-config [ file_path: string --editor: string = "" --debug = false ]: nothing -> nothing { if not ($file_path | path exists) { error make { msg: $"File not found: ($file_path)" } } # Check if encrypted if not (is-encrypted-config $file_path) { print $"⚠️ File is not encrypted, opening normally..." let editor_cmd = if ($editor | is-not-empty) { $editor } else { $env.EDITOR? | default "vim" } ^$editor_cmd $file_path return } # Use SOPS editor functionality (handles decrypt -> edit -> encrypt) print $"🔒 Opening encrypted file with SOPS editor..." # SOPS has built-in editor support let sops_config = (find-sops-config-path) if ($sops_config | is-not-empty) { ^sops --config $sops_config $file_path } else { ^sops $file_path } print $"✅ File saved and re-encrypted" } # Rotate encryption keys for a file export def rotate-encryption-keys [ file_path: string new_key_id: string --debug = false ]: nothing -> nothing { if not ($file_path | path exists) { error make { msg: $"File not found: ($file_path)" } } if not (is-encrypted-config $file_path) { error make { msg: $"File is not encrypted: ($file_path)" } } print $"🔄 Rotating encryption keys for ($file_path)" print $" New key: ($new_key_id)" # Create temporary decrypted file let temp_file = ($file_path | str replace ".yaml" ".tmp.yaml") try { # Decrypt to temp let decrypted = (on_sops "decrypt" $file_path) $decrypted | save --force $temp_file # Update SOPS config to use new key # This requires updating .sops.yaml to reference new key print $"⚠️ Manual step required: Update .sops.yaml with new key ($new_key_id)" print $" Then run: sops updatekeys ($file_path)" # Re-encrypt with new key ^sops updatekeys $file_path # Remove temp file rm --force $temp_file print $"✅ Key rotation completed" } catch { # Clean up temp file on error if ($temp_file | path exists) { rm --force $temp_file } error make { msg: $"Key rotation failed for ($file_path)" } } } # Validate encryption configuration export def validate-encryption-config []: nothing -> record { mut errors = [] mut warnings = [] # Check if SOPS is installed let sops_check = (^which sops | complete) if $sops_check.exit_code != 0 { $errors = ($errors | append { type: "missing_binary" severity: "error" message: "SOPS binary not found. Install with: brew install sops" }) } # Check if Age is installed (if using Age) let use_sops = (get-provisioning-use-sops) if ($use_sops | str contains "age") { let age_check = (^which age | complete) if $age_check.exit_code != 0 { $warnings = ($warnings | append { type: "missing_binary" severity: "warning" message: "Age binary not found. Install with: brew install age" }) } # Check for Age keys let age_key_file = (get-sops-age-key-file) if ($age_key_file | is-empty) { $warnings = ($warnings | append { type: "missing_key" severity: "warning" message: "Age key file not configured (PROVISIONING_KAGE)" }) } else if not ($age_key_file | path exists) { $errors = ($errors | append { type: "missing_key_file" severity: "error" message: $"Age key file not found: ($age_key_file)" }) } } # Check SOPS configuration file let sops_config = (find-sops-config-path) if ($sops_config | is-empty) { $warnings = ($warnings | append { type: "missing_config" severity: "warning" message: "SOPS config file (.sops.yaml) not found in project" }) } # Check KMS configuration if enabled let kms_enabled = (get-kms-enabled) if $kms_enabled { let kms_server = (get-kms-server) if ($kms_server | is-empty) { $errors = ($errors | append { type: "missing_kms_config" severity: "error" message: "KMS enabled but server URL not configured" }) } } { valid: (($errors | length) == 0) errors: $errors warnings: $warnings summary: { sops_installed: ($sops_check.exit_code == 0) age_backend: ($use_sops | str contains "age") kms_enabled: $kms_enabled total_errors: ($errors | length) total_warnings: ($warnings | length) } } } # Find SOPS configuration file def find-sops-config-path []: nothing -> string { # Check common locations let locations = [ ".sops.yaml" ".sops.yml" ($env.PWD | path join ".sops.yaml") ($env.HOME | path join ".config" | path join "provisioning" | path join "sops.yaml") (get-sops-config-path) ] for loc in $locations { if ($loc | path exists) { return $loc } } "" } # Check if config file contains sensitive data (heuristic) export def contains-sensitive-data [ file_path: string ]: nothing -> bool { if not ($file_path | path exists) { return false } let content = (open $file_path --raw) # Patterns that indicate sensitive data let sensitive_patterns = [ "password" "secret" "api_key" "token" "private_key" "credential" "auth" "access_key" "secret_key" ] for pattern in $sensitive_patterns { if ($content | str contains $pattern) { return true } } false } # Scan directory for unencrypted sensitive configs export def scan-unencrypted-configs [ directory: string --recursive = true ]: nothing -> table { mut results = [] let files = if $recursive { glob $"($directory)/**/*.{yaml,yml,toml,json}" } else { glob $"($directory)/*.{yaml,yml,toml,json}" } for file in $files { if (contains-sensitive-data $file) and not (is-encrypted-config $file) { $results = ($results | append { file: $file encrypted: false has_sensitive_data: true recommendation: "Should be encrypted" }) } } $results } # Encrypt all sensitive configs in directory export def encrypt-sensitive-configs [ directory: string --kms: string = "age" --dry-run = false --recursive = true ]: nothing -> nothing { print $"🔍 Scanning for unencrypted sensitive configs in ($directory)" let unencrypted = (scan-unencrypted-configs $directory --recursive=$recursive) if ($unencrypted | is-empty) { print $"✅ No unencrypted sensitive configs found" return } print $"\n📋 Found ($unencrypted | length) unencrypted sensitive configs:" print $unencrypted if $dry_run { print $"\n⚠️ Dry run mode - no files will be encrypted" return } print $"\n🔒 Encrypting files with ($kms)..." for file in ($unencrypted | get file) { print $" Encrypting: ($file)" encrypt-config $file --kms $kms --in-place } print $"\n✅ Encryption completed for all sensitive configs" } # Export key functions for CLI integration export def main [] { print "Configuration Encryption Module" print "================================" print "" print "Available commands:" print " - is-encrypted-config Check if config is encrypted" print " - load-encrypted-config Load and decrypt config" print " - encrypt-config Encrypt config file" print " - decrypt-config Decrypt config file" print " - edit-encrypted-config Edit encrypted config" print " - rotate-encryption-keys Rotate encryption keys" print " - validate-encryption-config Validate encryption setup" print " - scan-unencrypted-configs Find unencrypted sensitive configs" print " - encrypt-sensitive-configs Encrypt all sensitive configs" }