feat: rebuild xiteng.site homelab platform
This commit is contained in:
Executable
+201
@@ -0,0 +1,201 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user