- Updated `bf-wireguard-restart.yml` to include dynamic host targeting based on `operation_wave`. - Added pre-tasks to assert that the operation wave is approved and confirmed, ensuring proper execution context. - Modified the service restart task to use a more structured command format. - Adjusted `operations.py` and `ui_gui.py` to reflect changes in operation scope from "simple" to "wave" for relevant operations. - Improved UI handling in `ui_gui.py` and `ui_surfaces.py` for better output management and user interaction. - Updated log rotation configuration to target specific log files. - Enhanced systemd service configurations for WireGuard to improve restart behavior. - Refined firstboot scripts to ensure proper handling of dealer ID and state management. - Improved error handling and rollback mechanisms in identity and installation scripts.
276 lines
10 KiB
Bash
Executable file
276 lines
10 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# creds.sh — shared helpers for the Blueforce credential tooling (admin/bf-creds).
|
|
#
|
|
# Contract
|
|
# * No helper here ever writes a secret to stdout, stderr, or a log file.
|
|
# Secrets travel through files only: root-owned, mode 0600, inside a mode
|
|
# 0700 directory. Callers capture generated values with `$( ... )` (which
|
|
# never prints) and must never echo, printf, export, or log them.
|
|
# * Lookup helpers are read-only. Only the `bf_write_secret_file` /
|
|
# `bf_deliver_secret` helpers touch the filesystem, and only at the target
|
|
# the caller names explicitly.
|
|
# * A WireGuard *private key is never read* by this library: only its path is
|
|
# ever reported. The same rule applies to every other private key material.
|
|
# * This library does not change the remote-access decision recorded in
|
|
# docs/06-USERS-SSH-AND-PERMISSIONS.md: OpenSSH password authentication
|
|
# stays disabled (`PasswordAuthentication no`) and the `blueforce`
|
|
# maintenance account stays locked. bf-creds is an optional *local*
|
|
# delivery helper: it never edits sshd_config, never unlocks an account,
|
|
# and never opens a password login path. See the header of admin/bf-creds
|
|
# and the "admin/" section of README.md.
|
|
#
|
|
# Usage: sourced by admin/bf-creds. Not meant to be executed on its own.
|
|
#
|
|
set -euo pipefail
|
|
|
|
# Executing the library directly would silently do nothing useful; say so.
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
printf 'creds.sh is a library: source it (see admin/bf-creds).\n' >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [[ -n "${BF_CREDS_LIB_LOADED:-}" ]]; then
|
|
return 0
|
|
fi
|
|
BF_CREDS_LIB_LOADED=1
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Locations. Every path is overridable so tests can point the helpers at a
|
|
# sandbox and so a site can relocate the credential record deliberately.
|
|
# --------------------------------------------------------------------------- #
|
|
: "${BF_STATE_FILE:=/var/lib/blueforce/state.json}"
|
|
: "${BF_WG_DIR:=/etc/wireguard}"
|
|
: "${BF_RUSTDESK_ID_FILE:=/var/lib/blueforce/rustdesk-id}"
|
|
: "${BF_CREDENTIALS_FILE:=/root/blueforce-credentials.txt}"
|
|
: "${BF_SECRETS_SUBDIR:=admin/secrets}"
|
|
: "${BF_CREDENTIALS_MODE:=600}"
|
|
: "${BF_SECRETS_DIR_MODE:=700}"
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Logging. Messages are always descriptions, never values.
|
|
# --------------------------------------------------------------------------- #
|
|
bf_log() { printf '[bf-creds] %s\n' "$*" >&2; }
|
|
bf_warn() { printf '[bf-creds] WARN: %s\n' "$*" >&2; }
|
|
bf_die() { printf '[bf-creds] ERROR: %s\n' "$*" >&2; exit 1; }
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Read-only identity helpers.
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
# bf_detect_device_id -> prints BF-<8 digits> (BF_DEVICE, then the installer
|
|
# state file, then the hostname); prints nothing and returns 1 when unknown.
|
|
bf_detect_device_id() {
|
|
local id=""
|
|
if [[ "${BF_DEVICE:-}" =~ ^BF-[0-9]{8}$ ]]; then
|
|
printf '%s' "$BF_DEVICE"
|
|
return 0
|
|
fi
|
|
if [[ -r "$BF_STATE_FILE" ]] && grep -q 'device_id' "$BF_STATE_FILE" 2>/dev/null; then
|
|
id="$(grep -oE 'BF-[0-9]{8}' "$BF_STATE_FILE" 2>/dev/null | head -n 1 || true)"
|
|
fi
|
|
if [[ -z "$id" ]]; then
|
|
id="$(hostname 2>/dev/null | tr '[:lower:]' '[:upper:]' || true)"
|
|
fi
|
|
if [[ "$id" =~ ^BF-[0-9]{8}$ ]]; then
|
|
printf '%s' "$id"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# bf_dealer_number <device-id> -> prints the 8-digit dealer number, else nothing.
|
|
bf_dealer_number() {
|
|
local id="${1:-}"
|
|
if [[ "$id" =~ ^BF-([0-9]{8})$ ]]; then
|
|
printf '%s' "${BASH_REMATCH[1]}"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# bf_wireguard_public_key -> prints the public key (safe to display); nothing
|
|
# and return 1 when the installer has not created a keypair yet.
|
|
bf_wireguard_public_key() {
|
|
local f="$BF_WG_DIR/publickey" key=""
|
|
if [[ -s "$f" ]]; then
|
|
key="$(tr -d '[:space:]' < "$f" 2>/dev/null || true)"
|
|
fi
|
|
if [[ -n "$key" ]]; then
|
|
printf '%s' "$key"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# bf_wireguard_private_key_path -> prints the PATH of the private key only.
|
|
# The contents are never opened: only the file's existence is checked.
|
|
bf_wireguard_private_key_path() {
|
|
local f="$BF_WG_DIR/privatekey"
|
|
if [[ -e "$f" ]]; then
|
|
printf '%s' "$f"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# bf_rustdesk_id -> prints the RustDesk peer ID recorded by module 09-rustdesk.
|
|
bf_rustdesk_id() {
|
|
local f="$BF_RUSTDESK_ID_FILE" id=""
|
|
if [[ -s "$f" ]]; then
|
|
id="$(tr -d '[:space:]' < "$f" 2>/dev/null || true)"
|
|
fi
|
|
if [[ "$id" =~ ^[0-9]+$ ]]; then
|
|
printf '%s' "$id"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Secret generation and file handling.
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
# bf_generate_secret -> prints a fresh random secret on stdout (openssl, then
|
|
# /dev/urandom as a fallback). Capturing the result with `$( ... )` never
|
|
# prints it; the value must reach a mode-0600 file and nothing else. It is
|
|
# deliberately never passed through argv or a command line.
|
|
bf_generate_secret() {
|
|
local secret=""
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
secret="$(openssl rand -base64 24 2>/dev/null | tr -d '\n' || true)"
|
|
fi
|
|
if [[ -z "$secret" ]] && [[ -r /dev/urandom ]] && command -v base64 >/dev/null 2>&1; then
|
|
secret="$(head -c 32 /dev/urandom 2>/dev/null | base64 | tr -d '\n' || true)"
|
|
fi
|
|
if [[ -z "$secret" ]]; then
|
|
return 1
|
|
fi
|
|
printf '%s' "$secret"
|
|
}
|
|
|
|
# bf_file_mode <path> -> prints the octal mode (Linux stat).
|
|
bf_file_mode() { stat -c '%a' "$1" 2>/dev/null || true; }
|
|
|
|
# bf_has_generated_password <file> -> 0 when the record carries a password line.
|
|
bf_has_generated_password() {
|
|
local f="${1:-}"
|
|
if [[ -s "$f" ]] && grep -qE '^local_administrator_password:[[:space:]]*[^[:space:]]+' "$f"; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# bf_write_secret_file <dest> (stdin: entire file body)
|
|
# Writes stdin to <dest> atomically, forces mode 0600 on the file and 0700 on
|
|
# the parent directory, and verifies the result. Never echoes the body.
|
|
bf_write_secret_file() {
|
|
local dest="${1:-}" tmp="" parent="" node=""
|
|
if [[ -z "$dest" ]]; then
|
|
bf_warn 'bf_write_secret_file: destination required'
|
|
return 1
|
|
fi
|
|
# Refuse both a symlinked destination and a destination reached through a
|
|
# symlinked parent directory: lstat every ancestor up to /.
|
|
case "$dest" in
|
|
/*) node="$dest" ;;
|
|
*) node="$PWD/$dest" ;;
|
|
esac
|
|
while [[ "$node" != / && "$node" != . && -n "$node" ]]; do
|
|
if [[ -L "$node" ]]; then
|
|
bf_warn "refusing to write through a symlink: $node"
|
|
return 1
|
|
fi
|
|
parent="$(dirname "$node")"
|
|
[[ "$parent" == "$node" ]] && break
|
|
node="$parent"
|
|
done
|
|
if [[ -n "${BF_SECRETS_STAGING_DIR:-}" ]]; then
|
|
tmp="$(mktemp "${BF_SECRETS_STAGING_DIR}/bf-creds.XXXXXX" 2>/dev/null || true)"
|
|
else
|
|
# Default to a mode-700 directory under /var/lib/blueforce instead of a
|
|
# blind TMPDIR; TMPDIR is honored only as an explicit override knob.
|
|
if [[ -n "${TMPDIR:-}" ]]; then
|
|
tmp="$(mktemp "${TMPDIR}/bf-creds.XXXXXX" 2>/dev/null || true)"
|
|
else
|
|
install -d -m 700 /var/lib/blueforce/.creds-staging 2>/dev/null || true
|
|
tmp="$(mktemp "/var/lib/blueforce/.creds-staging/bf-creds.XXXXXX" 2>/dev/null || mktemp "/tmp/bf-creds.XXXXXX" 2>/dev/null || true)"
|
|
fi
|
|
fi
|
|
if [[ -z "$tmp" ]]; then
|
|
bf_warn 'cannot create a temporary staging file'
|
|
return 1
|
|
fi
|
|
# Interrupt safety: never leave staged secret material behind on a signal
|
|
# while a caller is still feeding stdin into `cat`. Normal and error paths
|
|
# below unlink explicitly. (No EXIT trap: this is a sourced library and
|
|
# must not clobber a caller's EXIT trap.)
|
|
trap 'rm -f "$tmp"' INT TERM HUP
|
|
chmod 600 "$tmp" || { rm -f "$tmp"; trap - INT TERM HUP; return 1; }
|
|
if ! cat > "$tmp"; then
|
|
rm -f "$tmp"; trap - INT TERM HUP
|
|
bf_warn 'cannot stage the credential body'
|
|
return 1
|
|
fi
|
|
if ! install -d -m "$BF_SECRETS_DIR_MODE" -- "$(dirname "$dest")"; then
|
|
rm -f "$tmp"; trap - INT TERM HUP
|
|
bf_warn "cannot create the credential directory: $(dirname "$dest")"
|
|
return 1
|
|
fi
|
|
if ! install -m "$BF_CREDENTIALS_MODE" -- "$tmp" "$dest"; then
|
|
rm -f "$tmp"; trap - INT TERM HUP
|
|
bf_warn "cannot write the credential file: $dest"
|
|
return 1
|
|
fi
|
|
rm -f "$tmp"; trap - INT TERM HUP
|
|
if [[ "$(bf_file_mode "$dest")" != "$BF_CREDENTIALS_MODE" ]]; then
|
|
bf_warn "credential file mode is not $BF_CREDENTIALS_MODE: $dest"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Delivery.
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
# bf_secrets_target_path <repo-root> <dealer-number> -> destination path.
|
|
bf_secrets_target_path() {
|
|
local repo_root="${1:-}" dealer="${2:-}"
|
|
if [[ -z "$repo_root" || -z "$dealer" ]]; then
|
|
return 1
|
|
fi
|
|
printf '%s/%s/%s.txt' "$repo_root" "$BF_SECRETS_SUBDIR" "$dealer"
|
|
}
|
|
|
|
# bf_deliver_secret <src> <dest>
|
|
# Copies a credential file to the delivery target: directory mode 0700, file
|
|
# mode 0600. Prints nothing; the caller reports the path only.
|
|
bf_deliver_secret() {
|
|
local src="${1:-}" dest="${2:-}"
|
|
if [[ ! -s "$src" ]]; then
|
|
bf_warn "credential file is missing or empty: $src"
|
|
return 1
|
|
fi
|
|
if [[ -z "$dest" ]]; then
|
|
bf_warn 'bf_deliver_secret: destination required'
|
|
return 1
|
|
fi
|
|
if [[ -L "$dest" ]]; then
|
|
bf_warn "refusing to write through a symlink: $dest"
|
|
return 1
|
|
fi
|
|
if ! install -d -m "$BF_SECRETS_DIR_MODE" -- "$(dirname "$dest")"; then
|
|
bf_warn "cannot create the delivery directory: $(dirname "$dest")"
|
|
return 1
|
|
fi
|
|
if ! install -m "$BF_CREDENTIALS_MODE" -- "$src" "$dest"; then
|
|
bf_warn "cannot deliver the credential file to: $dest"
|
|
return 1
|
|
fi
|
|
if [[ "$(bf_file_mode "$dest")" != "$BF_CREDENTIALS_MODE" ]]; then
|
|
bf_warn "delivered file mode is not $BF_CREDENTIALS_MODE: $dest"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|