|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# Secure AI Appliance — securectl (M23) |
| 4 | +# |
| 5 | +# Emergency data protection tool with three severity levels: |
| 6 | +# |
| 7 | +# Level 1 — Lock: |
| 8 | +# Lock vault, kill workers, require re-auth. Fully reversible. |
| 9 | +# |
| 10 | +# Level 2 — Wipe Keys: |
| 11 | +# Delete LUKS header backup, delete cosign keys, lock vault. |
| 12 | +# Data recoverable only with passphrase. |
| 13 | +# |
| 14 | +# Level 3 — Full Wipe: |
| 15 | +# Re-encrypt vault with random key (data unrecoverable), |
| 16 | +# zero inference memory, delete all logs, delete registry. |
| 17 | +# |
| 18 | +# Usage: |
| 19 | +# securectl panic <level> [--confirm <passphrase>] [--no-countdown] |
| 20 | +# securectl status |
| 21 | +# |
| 22 | +set -euo pipefail |
| 23 | + |
| 24 | +SECURE_AI_ROOT="/var/lib/secure-ai" |
| 25 | +AUTH_DIR="${SECURE_AI_ROOT}/auth" |
| 26 | +AUDIT_LOG="${SECURE_AI_ROOT}/logs/panic-audit.jsonl" |
| 27 | +PANIC_STATE="/run/secure-ai/panic-state.json" |
| 28 | +MAPPER_NAME="secure-ai-vault" |
| 29 | +MOUNT_POINT="${SECURE_AI_ROOT}" |
| 30 | + |
| 31 | +log() { |
| 32 | + echo "[securectl] $*" |
| 33 | + logger -t securectl "$*" 2>/dev/null || true |
| 34 | +} |
| 35 | + |
| 36 | +# --- Audit logging --- |
| 37 | +audit_panic() { |
| 38 | + local level="$1" |
| 39 | + local action="$2" |
| 40 | + local timestamp |
| 41 | + timestamp=$(date -Iseconds) |
| 42 | + |
| 43 | + mkdir -p "$(dirname "$AUDIT_LOG")" 2>/dev/null || true |
| 44 | + |
| 45 | + local entry |
| 46 | + entry=$(python3 -c " |
| 47 | +import json, hashlib |
| 48 | +entry = { |
| 49 | + 'timestamp': '${timestamp}', |
| 50 | + 'event': 'emergency_panic', |
| 51 | + 'severity': 'CRITICAL', |
| 52 | + 'level': ${level}, |
| 53 | + 'action': '${action}' |
| 54 | +} |
| 55 | +entry['hash'] = hashlib.sha256(json.dumps(entry, sort_keys=True).encode()).hexdigest() |
| 56 | +print(json.dumps(entry)) |
| 57 | +" 2>/dev/null || echo '{"event":"emergency_panic","level":'${level}'}') |
| 58 | + echo "$entry" >> "$AUDIT_LOG" 2>/dev/null || true |
| 59 | +} |
| 60 | + |
| 61 | +# --- Passphrase verification --- |
| 62 | +verify_passphrase() { |
| 63 | + local passphrase="$1" |
| 64 | + |
| 65 | + if [ ! -f "${AUTH_DIR}/passphrase.json" ]; then |
| 66 | + log "WARNING: no passphrase configured, skipping verification" |
| 67 | + return 0 |
| 68 | + fi |
| 69 | + |
| 70 | + # Verify against stored scrypt hash |
| 71 | + python3 -c " |
| 72 | +import json, hashlib, base64, sys |
| 73 | +try: |
| 74 | + with open('${AUTH_DIR}/passphrase.json') as f: |
| 75 | + data = json.load(f) |
| 76 | + stored_hash = data['hash'] |
| 77 | + salt = base64.b64decode(data['salt']) |
| 78 | + n = data.get('n', 32768) |
| 79 | + r = data.get('r', 8) |
| 80 | + p = data.get('p', 1) |
| 81 | + dk = hashlib.scrypt(sys.argv[1].encode(), salt=salt, n=n, r=r, p=p, dklen=64) |
| 82 | + if dk.hex() == stored_hash: |
| 83 | + sys.exit(0) |
| 84 | + else: |
| 85 | + sys.exit(1) |
| 86 | +except Exception as e: |
| 87 | + print(f'Verification error: {e}', file=sys.stderr) |
| 88 | + sys.exit(1) |
| 89 | +" "$passphrase" 2>/dev/null |
| 90 | +} |
| 91 | + |
| 92 | +# --- Countdown --- |
| 93 | +countdown() { |
| 94 | + local seconds="${1:-5}" |
| 95 | + echo "" |
| 96 | + echo " *** EMERGENCY ACTION IN ${seconds} SECONDS ***" |
| 97 | + echo " Press Ctrl+C to cancel." |
| 98 | + echo "" |
| 99 | + for i in $(seq "$seconds" -1 1); do |
| 100 | + echo " ${i}..." |
| 101 | + sleep 1 |
| 102 | + done |
| 103 | + echo "" |
| 104 | +} |
| 105 | + |
| 106 | +# --- Write panic state --- |
| 107 | +write_panic_state() { |
| 108 | + local level="$1" |
| 109 | + local status="$2" |
| 110 | + mkdir -p "$(dirname "$PANIC_STATE")" 2>/dev/null || true |
| 111 | + cat > "$PANIC_STATE" <<EOF |
| 112 | +{ |
| 113 | + "panic_active": true, |
| 114 | + "level": ${level}, |
| 115 | + "status": "${status}", |
| 116 | + "timestamp": "$(date -Iseconds)" |
| 117 | +} |
| 118 | +EOF |
| 119 | + chmod 644 "$PANIC_STATE" |
| 120 | +} |
| 121 | + |
| 122 | +# --- Level 1: Lock --- |
| 123 | +panic_level_1() { |
| 124 | + log "PANIC LEVEL 1: LOCK" |
| 125 | + audit_panic 1 "lock" |
| 126 | + write_panic_state 1 "locking" |
| 127 | + |
| 128 | + # Stop all AI services |
| 129 | + log "Stopping all AI services..." |
| 130 | + systemctl stop secure-ai-inference.service 2>/dev/null || true |
| 131 | + systemctl stop secure-ai-diffusion.service 2>/dev/null || true |
| 132 | + systemctl stop secure-ai-registry.service 2>/dev/null || true |
| 133 | + systemctl stop secure-ai-tool-firewall.service 2>/dev/null || true |
| 134 | + systemctl stop secure-ai-quarantine-watcher.service 2>/dev/null || true |
| 135 | + systemctl stop secure-ai-search-mediator.service 2>/dev/null || true |
| 136 | + |
| 137 | + # Kill any remaining workers |
| 138 | + pkill -f "llama-server" 2>/dev/null || true |
| 139 | + pkill -f "diffusion-worker" 2>/dev/null || true |
| 140 | + |
| 141 | + # Lock vault |
| 142 | + log "Locking vault..." |
| 143 | + sync |
| 144 | + umount "${MOUNT_POINT}" 2>/dev/null || true |
| 145 | + cryptsetup close "${MAPPER_NAME}" 2>/dev/null || true |
| 146 | + |
| 147 | + # Invalidate all sessions |
| 148 | + if [ -d "${AUTH_DIR}/sessions" ]; then |
| 149 | + rm -rf "${AUTH_DIR}/sessions"/* 2>/dev/null || true |
| 150 | + fi |
| 151 | + |
| 152 | + write_panic_state 1 "locked" |
| 153 | + log "LEVEL 1 COMPLETE: vault locked, services stopped, sessions invalidated" |
| 154 | +} |
| 155 | + |
| 156 | +# --- Level 2: Wipe Keys --- |
| 157 | +panic_level_2() { |
| 158 | + log "PANIC LEVEL 2: WIPE KEYS" |
| 159 | + audit_panic 2 "wipe_keys" |
| 160 | + write_panic_state 2 "wiping_keys" |
| 161 | + |
| 162 | + # First do everything Level 1 does |
| 163 | + panic_level_1 |
| 164 | + |
| 165 | + # Delete LUKS header backup |
| 166 | + log "Deleting LUKS header backup..." |
| 167 | + if [ -f "${SECURE_AI_ROOT}/keys/luks-header-backup" ]; then |
| 168 | + shred -vfz -n 3 "${SECURE_AI_ROOT}/keys/luks-header-backup" 2>/dev/null || true |
| 169 | + rm -f "${SECURE_AI_ROOT}/keys/luks-header-backup" 2>/dev/null || true |
| 170 | + fi |
| 171 | + |
| 172 | + # Delete cosign keys |
| 173 | + log "Deleting cosign signing keys..." |
| 174 | + find "${SECURE_AI_ROOT}/keys" -name "*.key" -o -name "*.pem" -o -name "*.pub" 2>/dev/null | while read -r keyfile; do |
| 175 | + shred -vfz -n 3 "$keyfile" 2>/dev/null || true |
| 176 | + rm -f "$keyfile" 2>/dev/null || true |
| 177 | + done |
| 178 | + |
| 179 | + # Delete TPM2 sealed keys |
| 180 | + if [ -d "${SECURE_AI_ROOT}/keys/tpm2" ]; then |
| 181 | + shred -vfz -n 3 "${SECURE_AI_ROOT}/keys/tpm2"/* 2>/dev/null || true |
| 182 | + rm -rf "${SECURE_AI_ROOT}/keys/tpm2" 2>/dev/null || true |
| 183 | + fi |
| 184 | + |
| 185 | + # Delete MOK private key |
| 186 | + find /etc/secure-ai -name "*.key" 2>/dev/null | while read -r keyfile; do |
| 187 | + shred -vfz -n 3 "$keyfile" 2>/dev/null || true |
| 188 | + rm -f "$keyfile" 2>/dev/null || true |
| 189 | + done |
| 190 | + |
| 191 | + write_panic_state 2 "keys_wiped" |
| 192 | + log "LEVEL 2 COMPLETE: all signing/encryption keys destroyed" |
| 193 | +} |
| 194 | + |
| 195 | +# --- Level 3: Full Wipe --- |
| 196 | +panic_level_3() { |
| 197 | + log "PANIC LEVEL 3: FULL WIPE" |
| 198 | + audit_panic 3 "full_wipe" |
| 199 | + write_panic_state 3 "full_wipe" |
| 200 | + |
| 201 | + # First do everything Level 2 does |
| 202 | + panic_level_2 |
| 203 | + |
| 204 | + # Re-encrypt vault with random key (makes data unrecoverable) |
| 205 | + log "Re-encrypting vault with random key..." |
| 206 | + local vault_device |
| 207 | + vault_device=$(awk -v name="${MAPPER_NAME}" '$1 == name {print $2}' /etc/crypttab 2>/dev/null || echo "") |
| 208 | + if [ -n "$vault_device" ] && [ -b "$vault_device" ]; then |
| 209 | + # Generate random passphrase and re-encrypt |
| 210 | + local random_key |
| 211 | + random_key=$(head -c 64 /dev/urandom | base64) |
| 212 | + echo -n "$random_key" | cryptsetup luksErase "$vault_device" --batch-mode 2>/dev/null || true |
| 213 | + log "Vault encryption key destroyed" |
| 214 | + fi |
| 215 | + |
| 216 | + # Zero inference memory (drop caches) |
| 217 | + log "Clearing memory..." |
| 218 | + echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true |
| 219 | + |
| 220 | + # Delete all logs |
| 221 | + log "Deleting all logs..." # This is the last log that will be written |
| 222 | + find "${SECURE_AI_ROOT}/logs" -type f -name "*.jsonl" -o -name "*.log" 2>/dev/null | while read -r logfile; do |
| 223 | + shred -vfz -n 1 "$logfile" 2>/dev/null || true |
| 224 | + rm -f "$logfile" 2>/dev/null || true |
| 225 | + done |
| 226 | + |
| 227 | + # Delete registry (models) |
| 228 | + log "Deleting model registry..." |
| 229 | + if [ -d "${SECURE_AI_ROOT}/registry" ]; then |
| 230 | + rm -rf "${SECURE_AI_ROOT}/registry"/* 2>/dev/null || true |
| 231 | + fi |
| 232 | + |
| 233 | + # Delete auth data |
| 234 | + if [ -d "${AUTH_DIR}" ]; then |
| 235 | + shred -vfz -n 1 "${AUTH_DIR}"/* 2>/dev/null || true |
| 236 | + rm -rf "${AUTH_DIR}"/* 2>/dev/null || true |
| 237 | + fi |
| 238 | + |
| 239 | + # Delete canary database |
| 240 | + shred -vfz -n 1 "${SECURE_AI_ROOT}/canary-db.json" 2>/dev/null || true |
| 241 | + rm -f "${SECURE_AI_ROOT}/canary-db.json" 2>/dev/null || true |
| 242 | + |
| 243 | + # Remove firstboot marker to force fresh setup |
| 244 | + rm -f "${SECURE_AI_ROOT}/.firstboot-done" 2>/dev/null || true |
| 245 | + |
| 246 | + write_panic_state 3 "wiped" |
| 247 | + log "LEVEL 3 COMPLETE: all data destroyed, system in factory-reset state" |
| 248 | +} |
| 249 | + |
| 250 | +# --- Status --- |
| 251 | +show_status() { |
| 252 | + if [ -f "$PANIC_STATE" ]; then |
| 253 | + cat "$PANIC_STATE" |
| 254 | + else |
| 255 | + echo '{"panic_active": false}' |
| 256 | + fi |
| 257 | +} |
| 258 | + |
| 259 | +# --- Main --- |
| 260 | +cmd="${1:-help}" |
| 261 | +shift || true |
| 262 | + |
| 263 | +case "$cmd" in |
| 264 | + panic) |
| 265 | + level="${1:-}" |
| 266 | + shift || true |
| 267 | + |
| 268 | + if [ -z "$level" ] || ! echo "$level" | grep -qE '^[123]$'; then |
| 269 | + echo "Usage: securectl panic <1|2|3> [--confirm <passphrase>] [--no-countdown]" |
| 270 | + echo "" |
| 271 | + echo "Levels:" |
| 272 | + echo " 1 — Lock vault, stop services (reversible)" |
| 273 | + echo " 2 — Wipe keys + Level 1 (data needs passphrase)" |
| 274 | + echo " 3 — Full wipe + Level 2 (data unrecoverable)" |
| 275 | + exit 1 |
| 276 | + fi |
| 277 | + |
| 278 | + # Parse options |
| 279 | + passphrase="" |
| 280 | + do_countdown="true" |
| 281 | + while [ $# -gt 0 ]; do |
| 282 | + case "$1" in |
| 283 | + --confirm) |
| 284 | + passphrase="${2:-}" |
| 285 | + shift 2 || { echo "ERROR: --confirm requires a passphrase"; exit 1; } |
| 286 | + ;; |
| 287 | + --no-countdown) |
| 288 | + do_countdown="false" |
| 289 | + shift |
| 290 | + ;; |
| 291 | + *) |
| 292 | + echo "Unknown option: $1" |
| 293 | + exit 1 |
| 294 | + ;; |
| 295 | + esac |
| 296 | + done |
| 297 | + |
| 298 | + # Level 2 and 3 require passphrase confirmation |
| 299 | + if [ "$level" -ge 2 ]; then |
| 300 | + if [ -z "$passphrase" ]; then |
| 301 | + echo "ERROR: Level ${level} requires --confirm <passphrase>" |
| 302 | + exit 1 |
| 303 | + fi |
| 304 | + if ! verify_passphrase "$passphrase"; then |
| 305 | + echo "ERROR: passphrase verification failed" |
| 306 | + exit 1 |
| 307 | + fi |
| 308 | + fi |
| 309 | + |
| 310 | + echo "" |
| 311 | + echo " ==========================================" |
| 312 | + echo " EMERGENCY PANIC — LEVEL ${level}" |
| 313 | + echo " ==========================================" |
| 314 | + case "$level" in |
| 315 | + 1) echo " Action: Lock vault, stop services" ;; |
| 316 | + 2) echo " Action: WIPE ALL KEYS + lock" ;; |
| 317 | + 3) echo " Action: FULL WIPE — DATA UNRECOVERABLE" ;; |
| 318 | + esac |
| 319 | + echo " ==========================================" |
| 320 | + |
| 321 | + if [ "$do_countdown" = "true" ]; then |
| 322 | + countdown 5 |
| 323 | + fi |
| 324 | + |
| 325 | + case "$level" in |
| 326 | + 1) panic_level_1 ;; |
| 327 | + 2) panic_level_2 ;; |
| 328 | + 3) panic_level_3 ;; |
| 329 | + esac |
| 330 | + ;; |
| 331 | + |
| 332 | + status) |
| 333 | + show_status |
| 334 | + ;; |
| 335 | + |
| 336 | + help|--help|-h) |
| 337 | + echo "securectl — Secure AI Appliance Emergency Control" |
| 338 | + echo "" |
| 339 | + echo "Commands:" |
| 340 | + echo " panic <1|2|3> Execute emergency action at specified level" |
| 341 | + echo " status Show current panic state" |
| 342 | + echo "" |
| 343 | + echo "Options:" |
| 344 | + echo " --confirm <passphrase> Required for levels 2 and 3" |
| 345 | + echo " --no-countdown Skip 5-second countdown" |
| 346 | + ;; |
| 347 | + |
| 348 | + *) |
| 349 | + echo "Unknown command: $cmd" |
| 350 | + echo "Run 'securectl help' for usage." |
| 351 | + exit 1 |
| 352 | + ;; |
| 353 | +esac |
0 commit comments