407 lines
12 KiB
Bash
Executable File
407 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
DEST="${VAULT_PASS_FILE:-$HOME/.config/vault-pass.txt}"
|
|
ARCHIVE="${1:-$REPO_DIR/secrets/vault-pass.txt.zip}"
|
|
VAULT_FILE="${VAULT_FILE:-$REPO_DIR/secrets/vault.yml}"
|
|
ENV_FILE="${INSTALL_ENV_FILE:-$REPO_DIR/install.env}"
|
|
|
|
load_env_file() {
|
|
if [ -f "$ENV_FILE" ]; then
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "$ENV_FILE"
|
|
set +a
|
|
fi
|
|
}
|
|
|
|
load_env_file
|
|
|
|
# Re-apply env-configurable paths after loading install.env.
|
|
DEST="${VAULT_PASS_FILE:-$HOME/.config/vault-pass.txt}"
|
|
ARCHIVE="${VAULT_PASS_ARCHIVE:-${1:-$REPO_DIR/secrets/vault-pass.txt.zip}}"
|
|
VAULT_FILE="${VAULT_FILE:-$REPO_DIR/secrets/vault.yml}"
|
|
# Optional non-interactive controls:
|
|
# INSTALL_VAULT_PASS_METHOD=create|manual|url|archive
|
|
# VAULT_PASS_CONTENT=<content> (for method=manual)
|
|
# VAULT_PASS_URL=<https-url> (for method=url)
|
|
# VAULT_PASS_ZIP_PASSWORD=<password> (for method=archive; avoid chat/log)
|
|
# VAULT_PASS_ZIP_PASSWORD_FILE=<path> (for method=archive; safer than env)
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage: scripts/install-vault-pass.sh [archive.zip]
|
|
|
|
Loads installer env from:
|
|
${INSTALL_ENV_FILE:-$REPO_DIR/install.env}
|
|
Override with:
|
|
INSTALL_ENV_FILE=/path/to/install.env ./scripts/install-vault-pass.sh
|
|
|
|
Installs the Ansible Vault password file to:
|
|
${VAULT_PASS_FILE:-$HOME/.config/vault-pass.txt}
|
|
|
|
Interactive behavior:
|
|
1. If the password file already exists, keep it and verify permissions.
|
|
2. If missing, prompt the user to choose one of four setup methods:
|
|
[1] Create a new vault password and initialize/re-encrypt vault.yml
|
|
[2] Paste/type vault-pass.txt content manually
|
|
[3] Download vault-pass.txt from a user-provided URL
|
|
[4] Extract vault-pass.txt from a password-protected zip archive
|
|
|
|
Non-interactive agent mode (via install.env or environment variables):
|
|
INSTALL_VAULT_PASS_METHOD=create ./scripts/install-vault-pass.sh
|
|
VAULT_PASS_CONTENT='...' INSTALL_VAULT_PASS_METHOD=manual ./scripts/install-vault-pass.sh
|
|
VAULT_PASS_URL='https://...' INSTALL_VAULT_PASS_METHOD=url ./scripts/install-vault-pass.sh
|
|
VAULT_PASS_ZIP_PASSWORD_FILE=/secure/pass INSTALL_VAULT_PASS_METHOD=archive ./scripts/install-vault-pass.sh
|
|
VAULT_PASS_ZIP_PASSWORD='...' INSTALL_VAULT_PASS_METHOD=archive ./scripts/install-vault-pass.sh
|
|
|
|
Check env sufficiency without installing:
|
|
./scripts/install-vault-pass.sh --check-env
|
|
|
|
Default archive path for method [4]:
|
|
$REPO_DIR/secrets/vault-pass.txt.zip
|
|
USAGE
|
|
}
|
|
|
|
ensure_dest_dir() {
|
|
umask 077
|
|
mkdir -p "$(dirname "$DEST")"
|
|
chmod 700 "$(dirname "$DEST")" || true
|
|
}
|
|
|
|
secure_dest() { chmod 600 "$DEST"; }
|
|
|
|
verify_existing() {
|
|
if [ -f "$DEST" ]; then
|
|
secure_dest
|
|
echo "Vault password file already exists: $DEST"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
require_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
echo "Missing dependency: $1" >&2
|
|
echo "Please install it first." >&2
|
|
exit 3
|
|
fi
|
|
}
|
|
|
|
create_new_password() {
|
|
require_cmd ansible-vault
|
|
require_cmd python3
|
|
ensure_dest_dir
|
|
umask 077
|
|
python3 - <<'PY' > "$DEST"
|
|
import secrets
|
|
print(secrets.token_urlsafe(48))
|
|
PY
|
|
secure_dest
|
|
echo "Created new vault password file: $DEST"
|
|
|
|
if [ -f "$VAULT_FILE" ]; then
|
|
if ansible-vault view "$VAULT_FILE" --vault-password-file "$DEST" >/dev/null 2>&1; then
|
|
echo "Existing vault is already readable with the new password. No re-encryption needed."
|
|
else
|
|
cat <<WARN
|
|
|
|
WARNING: $VAULT_FILE exists but is not readable with the new password.
|
|
To avoid destroying existing encrypted secrets, this script will NOT overwrite it automatically.
|
|
If this is a brand-new install, create a plaintext YAML file and run:
|
|
./scripts/vault.sh encrypt /path/to/plaintext.yml
|
|
If this is an existing vault, choose method [2], [3], or [4] with the correct password instead.
|
|
WARN
|
|
fi
|
|
else
|
|
mkdir -p "$(dirname "$VAULT_FILE")"
|
|
tmp="$(mktemp)"
|
|
chmod 600 "$tmp"
|
|
cat > "$tmp" <<'YAML'
|
|
# Initial placeholder vault. Replace with real secrets using ./scripts/vault.sh edit.
|
|
gitea: {}
|
|
openclaw_alice:
|
|
http_nodes: {}
|
|
ssh_nodes: {}
|
|
YAML
|
|
cp "$tmp" "$VAULT_FILE"
|
|
ansible-vault encrypt "$VAULT_FILE" --vault-password-file "$DEST"
|
|
rm -f "$tmp"
|
|
echo "Created encrypted placeholder vault: $VAULT_FILE"
|
|
fi
|
|
}
|
|
|
|
manual_create() {
|
|
ensure_dest_dir
|
|
if [ -n "${VAULT_PASS_CONTENT:-}" ]; then
|
|
umask 077
|
|
printf '%s\n' "$VAULT_PASS_CONTENT" > "$DEST"
|
|
else
|
|
cat <<MSG
|
|
Paste/type the vault password content now, then press Enter.
|
|
Input is hidden. The content will be written to:
|
|
$DEST
|
|
MSG
|
|
read -r -s pass
|
|
printf '\n'
|
|
if [ -z "$pass" ]; then
|
|
echo "Empty password is not allowed." >&2
|
|
exit 4
|
|
fi
|
|
umask 077
|
|
printf '%s\n' "$pass" > "$DEST"
|
|
fi
|
|
secure_dest
|
|
echo "Installed manually provided vault password file: $DEST"
|
|
}
|
|
|
|
download_from_url() {
|
|
ensure_dest_dir
|
|
url="${VAULT_PASS_URL:-}"
|
|
if [ -z "$url" ]; then
|
|
printf 'Enter vault-pass.txt URL: '
|
|
read -r url
|
|
fi
|
|
if [ -z "$url" ]; then
|
|
echo "URL is required." >&2
|
|
exit 4
|
|
fi
|
|
case "$url" in
|
|
http://*|https://*) ;;
|
|
*) echo "Only http:// or https:// URLs are supported." >&2; exit 4 ;;
|
|
esac
|
|
if command -v curl >/dev/null 2>&1; then
|
|
umask 077
|
|
curl -fsSL "$url" -o "$DEST"
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
umask 077
|
|
wget -qO "$DEST" "$url"
|
|
else
|
|
echo "Missing dependency: curl or wget" >&2
|
|
exit 3
|
|
fi
|
|
if [ ! -s "$DEST" ]; then
|
|
echo "Downloaded file is empty or missing." >&2
|
|
exit 4
|
|
fi
|
|
secure_dest
|
|
echo "Downloaded vault password file to: $DEST"
|
|
}
|
|
|
|
read_zip_password_file() {
|
|
# Bash strings cannot contain NUL bytes. Detect likely binary/invalid password
|
|
# files before command substitution so agents do not hit:
|
|
# warning: command substitution: ignored null byte in input
|
|
original_size="$(wc -c < "$VAULT_PASS_ZIP_PASSWORD_FILE" | tr -d '[:space:]')"
|
|
without_nul_size="$(LC_ALL=C tr -d '\000' < "$VAULT_PASS_ZIP_PASSWORD_FILE" | wc -c | tr -d '[:space:]')"
|
|
if [ "$original_size" != "$without_nul_size" ]; then
|
|
file_desc="$(file -b "$VAULT_PASS_ZIP_PASSWORD_FILE" 2>/dev/null || echo "unknown file type")"
|
|
cat >&2 <<ERR
|
|
VAULT_PASS_ZIP_PASSWORD_FILE appears to contain NUL bytes; provide a text password file instead.
|
|
|
|
Configured password-file path:
|
|
$VAULT_PASS_ZIP_PASSWORD_FILE
|
|
Detected file type:
|
|
$file_desc
|
|
|
|
This variable must point to a small plaintext file containing ONLY the zip archive password.
|
|
It must NOT point to:
|
|
- secrets/vault-pass.txt.zip (the archive itself)
|
|
- ~/.config/vault-pass.txt (the Ansible Vault password file)
|
|
- any binary/key/archive file
|
|
|
|
Fix one of these ways:
|
|
1. Create a plaintext zip-password file and set VAULT_PASS_ZIP_PASSWORD_FILE to that path.
|
|
2. Or set VAULT_PASS_ZIP_PASSWORD directly in a private local shell/env.
|
|
3. Or use INSTALL_VAULT_PASS_METHOD=url/manual/create instead of archive.
|
|
ERR
|
|
exit 4
|
|
fi
|
|
|
|
# Accept the first line and strip a trailing CR for files copied from Windows.
|
|
IFS= read -r zip_pass < "$VAULT_PASS_ZIP_PASSWORD_FILE" || true
|
|
zip_pass="${zip_pass%$'\r'}"
|
|
if [ -z "$zip_pass" ]; then
|
|
echo "VAULT_PASS_ZIP_PASSWORD_FILE is empty: $VAULT_PASS_ZIP_PASSWORD_FILE" >&2
|
|
exit 4
|
|
fi
|
|
}
|
|
|
|
extract_from_archive() {
|
|
require_cmd unzip
|
|
ensure_dest_dir
|
|
if [ ! -f "$ARCHIVE" ]; then
|
|
cat >&2 <<ERR
|
|
Missing archive: $ARCHIVE
|
|
|
|
Create/provide a password-protected archive that contains one file named:
|
|
vault-pass.txt
|
|
ERR
|
|
exit 2
|
|
fi
|
|
tmpdir="$(mktemp -d)"
|
|
cleanup() { rm -rf "$tmpdir"; }
|
|
trap cleanup EXIT
|
|
|
|
if [ -n "${VAULT_PASS_ZIP_PASSWORD_FILE:-}" ]; then
|
|
if [ ! -f "$VAULT_PASS_ZIP_PASSWORD_FILE" ]; then
|
|
echo "Missing VAULT_PASS_ZIP_PASSWORD_FILE: $VAULT_PASS_ZIP_PASSWORD_FILE" >&2
|
|
exit 4
|
|
fi
|
|
read_zip_password_file
|
|
unzip -P "$zip_pass" -q "$ARCHIVE" -d "$tmpdir"
|
|
elif [ -n "${VAULT_PASS_ZIP_PASSWORD:-}" ]; then
|
|
unzip -P "$VAULT_PASS_ZIP_PASSWORD" -q "$ARCHIVE" -d "$tmpdir"
|
|
else
|
|
# unzip will prompt for the archive password interactively.
|
|
unzip -q "$ARCHIVE" -d "$tmpdir"
|
|
fi
|
|
|
|
src="$tmpdir/vault-pass.txt"
|
|
if [ ! -f "$src" ]; then
|
|
echo "Archive extracted, but vault-pass.txt was not found inside." >&2
|
|
exit 4
|
|
fi
|
|
|
|
install -m 600 "$src" "$DEST"
|
|
echo "Installed vault password file from archive: $DEST"
|
|
}
|
|
|
|
verify_vault_readable_if_possible() {
|
|
if [ -f "$VAULT_FILE" ] && command -v ansible-vault >/dev/null 2>&1; then
|
|
if ansible-vault view "$VAULT_FILE" --vault-password-file "$DEST" >/dev/null 2>&1; then
|
|
echo "Verified: vault.yml is readable with $DEST"
|
|
else
|
|
echo "Warning: vault.yml is not readable with $DEST" >&2
|
|
return 5
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
preflight_env_config() {
|
|
if [ ! -f "$ENV_FILE" ]; then
|
|
echo "Installer env file not found: $ENV_FILE"
|
|
echo "Copy template first: cp install.env.example install.env"
|
|
return 0
|
|
fi
|
|
|
|
echo "Loaded installer env: $ENV_FILE"
|
|
|
|
if [ -f "$DEST" ]; then
|
|
echo "Preflight: vault password file already exists: $DEST"
|
|
return 0
|
|
fi
|
|
|
|
method="${INSTALL_VAULT_PASS_METHOD:-}"
|
|
if [ -z "$method" ]; then
|
|
echo "Preflight: install.env does not set INSTALL_VAULT_PASS_METHOD; interactive menu will be used."
|
|
return 0
|
|
fi
|
|
|
|
case "$method" in
|
|
create|1)
|
|
echo "Preflight: install.env is sufficient for method=create."
|
|
;;
|
|
manual|2)
|
|
if [ -n "${VAULT_PASS_CONTENT:-}" ]; then
|
|
echo "Preflight: install.env is sufficient for method=manual (VAULT_PASS_CONTENT set)."
|
|
else
|
|
echo "Preflight: method=manual but VAULT_PASS_CONTENT is empty; hidden input will be required."
|
|
fi
|
|
;;
|
|
url|3)
|
|
if [ -n "${VAULT_PASS_URL:-}" ]; then
|
|
echo "Preflight: install.env is sufficient for method=url."
|
|
else
|
|
echo "Preflight: method=url but VAULT_PASS_URL is empty; URL input will be required."
|
|
fi
|
|
;;
|
|
archive|4)
|
|
if [ -n "${VAULT_PASS_ZIP_PASSWORD_FILE:-}" ] && [ -f "$VAULT_PASS_ZIP_PASSWORD_FILE" ]; then
|
|
echo "Preflight: install.env is sufficient for method=archive (password file exists)."
|
|
elif [ -n "${VAULT_PASS_ZIP_PASSWORD_FILE:-}" ]; then
|
|
echo "Preflight: method=archive but VAULT_PASS_ZIP_PASSWORD_FILE does not exist: $VAULT_PASS_ZIP_PASSWORD_FILE"
|
|
elif [ -n "${VAULT_PASS_ZIP_PASSWORD:-}" ]; then
|
|
echo "Preflight: install.env is sufficient for method=archive (inline zip password set)."
|
|
else
|
|
echo "Preflight: method=archive but no zip password is configured; unzip will prompt interactively."
|
|
fi
|
|
if [ ! -f "$ARCHIVE" ]; then
|
|
echo "Preflight: archive file is missing: $ARCHIVE"
|
|
fi
|
|
;;
|
|
*)
|
|
echo "Preflight: invalid INSTALL_VAULT_PASS_METHOD: $method"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
env_has_noninteractive_config() {
|
|
method="${INSTALL_VAULT_PASS_METHOD:-}"
|
|
case "$method" in
|
|
create|1) return 0 ;;
|
|
manual|2) [ -n "${VAULT_PASS_CONTENT:-}" ] ;;
|
|
url|3) [ -n "${VAULT_PASS_URL:-}" ] ;;
|
|
archive|4)
|
|
{ [ -n "${VAULT_PASS_ZIP_PASSWORD:-}" ] || { [ -n "${VAULT_PASS_ZIP_PASSWORD_FILE:-}" ] && [ -f "$VAULT_PASS_ZIP_PASSWORD_FILE" ]; }; } && [ -f "$ARCHIVE" ]
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
run_method() {
|
|
case "$1" in
|
|
create|1) create_new_password ;;
|
|
manual|2) manual_create ;;
|
|
url|3) download_from_url ;;
|
|
archive|4) extract_from_archive ;;
|
|
*) echo "Invalid setup method: $1" >&2; exit 4 ;;
|
|
esac
|
|
}
|
|
|
|
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
|
|
if [ "${1:-}" = "--check-env" ]; then
|
|
preflight_env_config
|
|
exit 0
|
|
fi
|
|
|
|
if verify_existing; then
|
|
verify_vault_readable_if_possible || true
|
|
exit 0
|
|
fi
|
|
|
|
preflight_env_config
|
|
|
|
if [ -n "${INSTALL_VAULT_PASS_METHOD:-}" ]; then
|
|
if env_has_noninteractive_config; then
|
|
echo "Using non-interactive configuration from env."
|
|
else
|
|
echo "Env is not sufficient for a fully non-interactive install; installer may prompt."
|
|
fi
|
|
run_method "$INSTALL_VAULT_PASS_METHOD"
|
|
verify_vault_readable_if_possible || true
|
|
exit 0
|
|
fi
|
|
|
|
cat <<MENU
|
|
Vault password file does not exist:
|
|
$DEST
|
|
|
|
Choose setup method:
|
|
1) Create a new vault password and initialize/re-encrypt vault.yml if needed
|
|
2) Paste/type vault-pass.txt content manually
|
|
3) Download vault-pass.txt from a URL
|
|
4) Extract vault-pass.txt from password-protected zip archive
|
|
MENU
|
|
printf 'Enter choice [1-4]: '
|
|
read -r choice
|
|
|
|
run_method "$choice"
|
|
verify_vault_readable_if_possible || true
|