Jesús Pérez 1fe83246d6
feat: integrate enterprise security system into core libraries
Adds KMS, secrets management, config encryption, and auth plugins to enable
zero-trust security architecture across the provisioning platform.
2025-10-09 16:36:27 +01:00

506 lines
14 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <file> Check if config is encrypted"
print " - load-encrypted-config <file> Load and decrypt config"
print " - encrypt-config <file> Encrypt config file"
print " - decrypt-config <file> Decrypt config file"
print " - edit-encrypted-config <file> Edit encrypted config"
print " - rotate-encryption-keys <file> Rotate encryption keys"
print " - validate-encryption-config Validate encryption setup"
print " - scan-unencrypted-configs <dir> Find unencrypted sensitive configs"
print " - encrypt-sensitive-configs <dir> Encrypt all sensitive configs"
}