Files
homelab/homelab-emergency
T

202 lines
6.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
AUTHENTIK_COMPOSE="$SCRIPT_DIR/authentik/compose.yml"
VAULT_COMPOSE="$SCRIPT_DIR/ai-gateway/compose.yml"
usage() {
cat <<'EOF'
Usage: ./homelab-emergency <command> [arguments]
Bootstrap and configuration:
init-secrets
identity-bootstrap
Recovery:
status
identity-recovery [username] default: liooil
identity-set-password [username] default: liooil
identity-reset-2fa [username] delete all TOTP devices; default: liooil
identity-reset-passkeys [username] delete all Passkeys; default: liooil
vault-list [owner-sub]
vault-audit [limit]
vault-verify <credential-id>
vault-delete <credential-id>
vault-reassign <credential-id> <issuer> <sub> <username>
vault-backup <destination.db>
vault-rotate-key <new-key-file>
The script never prints stored provider credentials.
EOF
}
require_command() {
command -v "$1" >/dev/null 2>&1 || {
echo "Required command not found: $1" >&2
exit 1
}
}
ensure_parent() {
mkdir -p -- "$(dirname -- "$1")"
}
generate_base64_key() {
local destination=$1
if [[ -e "$destination" ]]; then
echo "Keeping existing secret: $destination"
return
fi
ensure_parent "$destination"
chmod 700 -- "$(dirname -- "$destination")"
openssl rand 32 | base64 -w 0 >"$destination"
chmod 600 -- "$destination"
echo "Created: $destination"
}
vault_exec() {
if docker ps --format '{{.Names}}' | grep -Fxq ai-gateway; then
docker compose -f "$VAULT_COMPOSE" exec -T ai-gateway node /app/cli.mjs "$@"
else
docker compose -f "$VAULT_COMPOSE" run --rm --no-deps ai-gateway node /app/cli.mjs "$@"
fi
}
identity_reset_authenticators() {
local username=$1
local model_module=$2
local model_name=$3
docker compose -f "$AUTHENTIK_COMPOSE" exec -T -e TARGET_USERNAME="$username" authentik \
ak shell -c "import os; from authentik.core.models import User; from $model_module import $model_name; user=User.objects.get(username=os.environ['TARGET_USERNAME']); print($model_name.objects.filter(user=user).delete()[0])"
}
identity_audit() {
local action=$1
local username=$2
local encoded
encoded=$(printf '%s' "$username" | base64 -w 0)
mkdir -p -- "$SCRIPT_DIR/site/data"
printf '{"createdAt":"%s","actor":"homelab-emergency","action":"%s","target":"base64:%s","detail":"{}"}\n' \
"$(date --iso-8601=seconds)" "$action" "$encoded" >>"$SCRIPT_DIR/site/data/identity-audit.jsonl"
chmod 600 -- "$SCRIPT_DIR/site/data/identity-audit.jsonl"
}
command_name=${1:-}
shift || true
case "$command_name" in
init-secrets)
require_command openssl
require_command base64
umask 077
generate_base64_key "$SCRIPT_DIR/ai-gateway/secrets/vault_master_key"
generate_base64_key "$SCRIPT_DIR/ai-gateway/secrets/portal_gateway_hmac"
;;
identity-bootstrap)
docker compose -f "$AUTHENTIK_COMPOSE" exec -T authentik \
ak shell -c "exec(open('/bootstrap/portal_identity.py').read())"
docker compose -f "$SCRIPT_DIR/site/compose.yml" up -d --force-recreate xiteng-site
;;
identity-recovery)
username=${1:-liooil}
docker compose -f "$AUTHENTIK_COMPOSE" exec -T authentik ak create_recovery_key 60 "$username" -v 0
;;
identity-set-password)
username=${1:-liooil}
docker compose -f "$AUTHENTIK_COMPOSE" exec authentik ak changepassword "$username"
;;
identity-reset-2fa)
username=${1:-liooil}
read -r -p "Delete every TOTP device for $username? [y/N] " confirm
[[ "$confirm" == "y" || "$confirm" == "Y" ]] || exit 1
identity_reset_authenticators "$username" "authentik.stages.authenticator_totp.models" "TOTPDevice"
identity_audit "emergency.totp.reset" "$username"
;;
identity-reset-passkeys)
username=${1:-liooil}
read -r -p "Delete every Passkey for $username? [y/N] " confirm
[[ "$confirm" == "y" || "$confirm" == "Y" ]] || exit 1
identity_reset_authenticators "$username" "authentik.stages.authenticator_webauthn.models" "WebAuthnDevice"
identity_audit "emergency.passkeys.reset" "$username"
;;
status)
docker compose -f "$AUTHENTIK_COMPOSE" ps authentik worker postgres redis
docker compose -f "$VAULT_COMPOSE" ps
;;
vault-list)
vault_exec list "$@"
;;
vault-audit)
vault_exec audit "$@"
;;
vault-verify)
vault_exec verify "$@"
;;
vault-delete)
credential_id=${1:-}
if [[ -z "$credential_id" ]]; then
echo "Credential ID is required" >&2
exit 1
fi
read -r -p "Permanently delete credential $credential_id? [y/N] " confirm
[[ "$confirm" == "y" || "$confirm" == "Y" ]] || exit 1
vault_exec delete "$credential_id"
;;
vault-reassign)
if [[ $# -lt 4 ]]; then
echo "credential-id, issuer, sub and username are required" >&2
exit 1
fi
vault_exec reassign "$@"
;;
vault-backup)
destination=${1:-}
if [[ -z "$destination" ]]; then
echo "Destination DB path is required" >&2
exit 1
fi
absolute_destination=$(realpath -m -- "$destination")
case "$absolute_destination" in
"$SCRIPT_DIR/ai-gateway/data/"*) ;;
*)
echo "Vault CLI backup target must be under ai-gateway/data so the container can write it." >&2
echo "Suggested: ai-gateway/data/backups/vault-$(date +%Y%m%d-%H%M%S).db" >&2
exit 1
;;
esac
relative_destination=/data/${absolute_destination#"$SCRIPT_DIR/ai-gateway/data/"}
ensure_parent "$absolute_destination"
vault_exec backup "$relative_destination"
chmod 600 -- "$absolute_destination"
echo "Vault backup written to $absolute_destination"
;;
vault-rotate-key)
new_key_file=${1:-}
if [[ -z "$new_key_file" || ! -f "$new_key_file" ]]; then
echo "A readable new key file is required" >&2
exit 1
fi
read -r -p "Rewrap every Vault data key and then replace vault_master_key? [y/N] " confirm
[[ "$confirm" == "y" || "$confirm" == "Y" ]] || exit 1
temporary_key="$SCRIPT_DIR/ai-gateway/secrets/vault_master_key.next"
cp -- "$new_key_file" "$temporary_key"
chmod 600 -- "$temporary_key"
vault_exec rotate-master "/run/secrets/vault_master_key.next"
mv -- "$temporary_key" "$SCRIPT_DIR/ai-gateway/secrets/vault_master_key"
chmod 600 -- "$SCRIPT_DIR/ai-gateway/secrets/vault_master_key"
docker compose -f "$VAULT_COMPOSE" restart ai-gateway
echo "Vault master key rotated and service restarted. Back up the new key now."
;;
""|-h|--help|help)
usage
;;
*)
echo "Unknown command: $command_name" >&2
usage >&2
exit 1
;;
esac