275 lines
7.7 KiB
Text
275 lines
7.7 KiB
Text
# 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
|
|
}
|
|
}
|
|
}
|