provisioning-core/nulib/domain/config/cache/sops.nu

276 lines
7.7 KiB
Text
Raw Permalink Normal View History

# SOPS Decryption Cache System
# Caches decrypted SOPS content with strict 0600 permissions
# SECURITY: All SOPS cache files must have 0600 permissions
# TTL: 15 minutes (configurable, balances security and performance)
# Follows Nushell 0.109.0+ guidelines
# Selective imports (ADR-025 Phase 3 Layer 2).
# cache/metadata star-import was dead — dropped.
use domain/cache/core.nu [cache-clear-type cache-lookup cache-write]
# Helper: Compute hash of SOPS file path
def compute-sops-hash [file_path: string] {
let hash_result = (do {
$file_path | ^openssl dgst -sha256 -hex
} | complete)
if $hash_result.exit_code == 0 {
($hash_result.stdout | str trim | split column " " | get column1 | get 0)
} else {
($file_path | hash md5 | str substring 0..32)
}
}
# Helper: Set file permissions to 0600 (owner read-write only)
def set-sops-file-permissions [cache_file: string] {
do {
^chmod 0600 $cache_file
} | complete | ignore
}
# Helper: Set directory permissions to 0700 (owner read-write-execute)
def set-sops-dir-permissions [cache_dir: string] {
do {
^chmod 0700 $cache_dir
} | complete | ignore
}
# Helper: Validate file permissions
def validate-sops-permissions [cache_file: string] {
let stat_result = (do {
if ($cache_file | path exists) {
$cache_file | stat
} else {
error make { msg: "File not found" }
}
} | complete)
if $stat_result.exit_code != 0 {
return { valid: false, reason: "file_stat_error" }
}
let perms = ($stat_result.stdout | get permissions)
# Check for 0600 permissions (read/write for owner only)
# Acceptable: -rw------- or similar
if ((not ($perms | str contains "rw")) or
(($perms | str length) > 1 and (($perms | str substring 4..-1) != "------"))) {
return { valid: false, reason: "permissions_too_permissive" }
}
{ valid: true, reason: "permissions_valid" }
}
# ============================================================================
# PUBLIC API: SOPS Cache Operations
# ============================================================================
# Cache decrypted SOPS content with security
export def cache-sops-decrypt [
file_path: string
decrypted_content: string
] {
let cache_key = (compute-sops-hash $file_path)
let source_files = [$file_path]
# Write cache with 15-minute TTL (security sensitive)
cache-write "sops" $cache_key $decrypted_content $source_files --ttl 900
# CRITICAL: Set 0600 permissions on cache file
let cache_file = (let home = ($env.HOME? | default "~" | path expand);
$home | path join ".provisioning" "cache" "config" "sops" $cache_key)
if ($cache_file | path exists) {
set-sops-file-permissions $cache_file
}
# Set directory permissions to 0700
let cache_dir = ($cache_file | path dirname)
if ($cache_dir | path exists) {
set-sops-dir-permissions $cache_dir
}
}
# Lookup cached SOPS decryption
export def lookup-sops-cache [
file_path: string
] {
if not ($file_path | path exists) {
return { valid: false, reason: "file_not_found", data: null }
}
let cache_key = (compute-sops-hash $file_path)
# Try to lookup in cache
let cache_result = (cache-lookup "sops" $cache_key)
if not $cache_result.valid {
return {
valid: false,
reason: $cache_result.reason,
data: null
}
}
# SECURITY: Validate permissions before returning
let home = ($env.HOME? | default "~" | path expand)
let cache_file = ($home | path join ".provisioning" "cache" "config" "sops" $cache_key)
let perm_check = (validate-sops-permissions $cache_file)
if not $perm_check.valid {
# Permissions compromised - invalidate cache
return {
valid: false,
reason: $"permission_violation: ($perm_check.reason)",
data: null
}
}
{
valid: true,
reason: "cache_hit",
data: $cache_result.data
}
}
# Validate SOPS cache (permissions + TTL + mtime)
def validate-sops-cache [
cache_file: string
] {
# Check permissions
let perm_check = (validate-sops-permissions $cache_file)
if not $perm_check.valid {
return $perm_check
}
# Load metadata
let meta_file = $"($cache_file).meta"
let meta_load = (do {
open $meta_file
} | complete)
if $meta_load.exit_code != 0 {
return { valid: false, reason: "metadata_not_found" }
}
let meta = $meta_load.stdout
# Check TTL
let now = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
if $now > $meta.expires_at {
return { valid: false, reason: "ttl_expired" }
}
# Check source file mtime
for src_file in $meta.source_files {
let current_mtime = (do {
if ($src_file | path exists) {
$src_file | stat | get modified | into int
} else {
-1
}
} | complete | get stdout)
let cached_mtime = ($meta.source_mtimes | get --optional $src_file | default (-1))
if $current_mtime != $cached_mtime {
return { valid: false, reason: "sops_file_modified" }
}
}
{ valid: true, reason: "validation_passed" }
}
# Clear SOPS cache (security cleanup)
export def clear-sops-cache [
--pattern: string = "*" # Optional: clear specific pattern
] {
cache-clear-type "sops"
}
# Rotate SOPS cache (refresh permissions and TTL)
export def rotate-sops-cache [
file_path: string
decrypted_content: string
] {
# Clear old cache
let old_cache_key = (compute-sops-hash $file_path)
let home = ($env.HOME? | default "~" | path expand)
let old_cache_file = ($home | path join ".provisioning" "cache" "config" "sops" $old_cache_key)
do {
rm -f $old_cache_file
rm -f $"($old_cache_file).meta"
} | complete | ignore
# Write new cache with refreshed TTL
cache-sops-decrypt $file_path $decrypted_content
}
# Get SOPS cache statistics
export def get-sops-cache-stats [] {
let home = ($env.HOME? | default "~" | path expand)
let base = ($home | path join ".provisioning" "cache" "config" "sops")
if not ($base | path exists) {
return {
total_entries: 0,
total_size_mb: 0,
permission_errors: 0
}
}
mut stats = {
total_entries: 0,
total_size_mb: 0,
permission_errors: 0
}
for meta_file in (glob $"($base)/**/*.meta") {
let cache_file = ($meta_file | str substring 0..-6)
if ($cache_file | path exists) {
# Check permissions
let perm_check = (validate-sops-permissions $cache_file)
if not $perm_check.valid {
$stats.permission_errors += 1
}
let size_result = (do {
$cache_file | stat | get size
} | complete)
if $size_result.exit_code == 0 {
let size_mb = ($size_result.stdout / 1048576)
$stats.total_entries += 1
$stats.total_size_mb += $size_mb
}
}
}
$stats
}
# Enforce permissions on all SOPS cache files (security audit)
export def enforce-sops-permissions [] {
let home = ($env.HOME? | default "~" | path expand)
let base = ($home | path join ".provisioning" "cache" "config" "sops")
if not ($base | path exists) {
return
}
# Enforce directory permissions
set-sops-dir-permissions $base
# Enforce file permissions
for cache_file in (glob $"($base)/*" --no-dir) {
if ($cache_file | path exists) {
set-sops-file-permissions $cache_file
}
}
}