Simplifies the primary path from "build a custom ISO" to "install from git and run one menu", which is what the operator actually asked for. The ISO tooling stays in the tree as a secondary/optional route and is not removed. admin/bf-bootstrap.sh -- one command on Ubuntu: - clones the repo to /opt/blueforce-linux or fast-forwards an existing checkout (a dirty tree is reported and skipped, never reset or stashed) - installs git/curl/ansible-core, links admin/bf-* and scripts/*/bf-* into /usr/local/bin, and finishes with the exact next command - --check is read-only, works without root, and mutates nothing; the piped form (curl ... | sudo -S -p '' bash) is guarded against eating the script admin/bf-menu -- numbered operator console (categories 1-5): - 1 installs THIS device locally; 2 and 5 drive the fleet through the existing 21 Ansible playbooks; 3 prepares and verifies central integration without installing a central server; 4 holds local tools - every operation picks one target: a dealer number or an approved wave group. Scope is derived from the playbook file itself, so a playbook whose declared scope disagrees with the console's table is refused rather than run - the command is always shown before it runs; release-gated operations require a typed wave token, production requires a second token, and "all" runs wave-by-wave, stopping at the first failure - audit log at /var/log/blueforce-console.log (mode 600, falls back to $XDG_STATE_HOME when not writable), recording sanitized target/command/exit only: no playbook output, no secrets admin/bf-creds -- optional credential record, DEFAULT OFF: - a bare run generates nothing and reports what already exists (device id, WireGuard public key, private key path only, RustDesk id) - --generate writes /root/blueforce-credentials.txt (0600) and --fetch delivers it to admin/secrets/<dealer>.txt (0700/0600, git-ignored). The secret never reaches stdout, stderr or argv - this does not change the SSH design: password auth stays off and the blueforce account stays locked, per docs/06. It is a local delivery aid only docs/33-OPERATOR-CONSOLE.md -- step-by-step guide for an operator new to Linux and DevOps: which command to type where, what the screen will ask, both end-to-end sample sessions, the five most common errors, and the security rules. Tests: run-all.sh -> 9/9 pass, exit 0 (adds check-admin-static.sh, 159 checks, with negative controls and mutation-proven non-vacuity).
236 lines
9.6 KiB
Bash
Executable file
236 lines
9.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# bf-creds — optional, default-OFF credential generation and local delivery.
|
|
#
|
|
# Scope (deliberate, decided with the operator)
|
|
# * DEFAULT BEHAVIOUR: nothing is generated. Unless `--generate` (or
|
|
# BF_GENERATE_CREDENTIALS=1) is given, no password is created and the
|
|
# command says so explicitly.
|
|
# * With `--generate`: a device credential record is written to
|
|
# /root/blueforce-credentials.txt with mode 0600. It carries the device
|
|
# identity (BF-<no>), the hostname, the creation time, the WireGuard
|
|
# PUBLIC key plus the private key PATH (never its contents), the RustDesk
|
|
# ID when the installer recorded one, and a freshly generated local
|
|
# administrator password.
|
|
# * With `--fetch`: the record is copied to
|
|
# <repo>/admin/secrets/<dealer-number>.txt (directory 0700, file 0600).
|
|
# That path is git-ignored; it must never be committed.
|
|
# * Secrets are written to files only. No password is ever printed to
|
|
# stdout/stderr, written to a log, or passed through argv — so it cannot
|
|
# land in the shell history or the process list.
|
|
#
|
|
# What this tool does NOT do
|
|
# Remote access stays exactly as the installer left it: OpenSSH password
|
|
# authentication stays disabled (`PasswordAuthentication no`,
|
|
# docs/06-USERS-SSH-AND-PERMISSIONS.md, docs/11-SECURITY-HARDENING.md) and
|
|
# the `blueforce` maintenance account stays locked. bf-creds never edits
|
|
# sshd_config, never unlocks an account, and never enables a password login
|
|
# path; it is only an optional *local* delivery helper. See README.md
|
|
# ("admin/" section).
|
|
#
|
|
# Usage: sudo bf-creds [--generate] [--fetch] [--repo DIR] [--dealer-id N] [--help]
|
|
#
|
|
set -euo pipefail
|
|
|
|
SELF="${BASH_SOURCE[0]:-}"
|
|
SELF_REAL="$(readlink -f "$SELF" 2>/dev/null || printf '%s' "$SELF")"
|
|
SCRIPT_DIR="$(cd "$(dirname "$SELF_REAL")" && pwd)"
|
|
# shellcheck source=lib/creds.sh
|
|
source "$SCRIPT_DIR/lib/creds.sh"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
GENERATE=0
|
|
FETCH=0
|
|
REPO_OVERRIDE=""
|
|
DEALER_OVERRIDE=""
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: sudo bf-creds [--generate] [--fetch] [--repo DIR] [--dealer-id NNNNNNNN] [--help]
|
|
|
|
Report the device identity and the secrets that already exist on this host, and
|
|
optionally generate a local credential record and deliver it into the repository
|
|
checkout.
|
|
|
|
Options:
|
|
--generate Create the credential record (default path:
|
|
/root/blueforce-credentials.txt, mode 0600). Without this
|
|
flag NO password is generated and the command says so.
|
|
--fetch Copy the credential record to <repo>/admin/secrets/<dealer>.txt
|
|
(directory 0700, file 0600, git-ignored).
|
|
--repo DIR Repository root used as the --fetch destination
|
|
(default: the checkout this tool lives in).
|
|
--dealer-id N Eight-digit dealer number. Default: detected from the device
|
|
identity (installer state file, then the hostname).
|
|
--help, -h Show this help and exit.
|
|
|
|
Environment:
|
|
BF_GENERATE_CREDENTIALS=1 Same as --generate (still opt-in; the default is off).
|
|
BF_CREDENTIALS_FILE=PATH Override the credential record path.
|
|
BF_WG_DIR=PATH Override the WireGuard directory.
|
|
BF_STATE_FILE=PATH Override the installer state file.
|
|
BF_RUSTDESK_ID_FILE=PATH Override the recorded RustDesk ID file.
|
|
|
|
Guarantees:
|
|
* The generated password is written to the record file only. It is never
|
|
printed, never logged, and never placed on a command line.
|
|
* The WireGuard private key is never read: only its path is reported.
|
|
* Remote access is not changed: SSH password authentication stays disabled
|
|
and the `blueforce` account stays locked.
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--generate) GENERATE=1; shift ;;
|
|
--fetch) FETCH=1; shift ;;
|
|
--repo) REPO_OVERRIDE="${2:?missing value for --repo}"; shift 2 ;;
|
|
--dealer-id) DEALER_OVERRIDE="${2:?missing value for --dealer-id}"; shift 2 ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) printf 'ERROR: unknown argument: %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ "${BF_GENERATE_CREDENTIALS:-0}" == "1" ]]; then
|
|
GENERATE=1
|
|
fi
|
|
if [[ -n "$DEALER_OVERRIDE" ]] && [[ ! "$DEALER_OVERRIDE" =~ ^[0-9]{8}$ ]]; then
|
|
printf 'ERROR: --dealer-id must be exactly 8 digits\n' >&2
|
|
exit 2
|
|
fi
|
|
if [[ -n "$REPO_OVERRIDE" ]]; then
|
|
if [[ ! -d "$REPO_OVERRIDE" ]]; then
|
|
printf 'ERROR: --repo must name an existing directory: %s\n' "$REPO_OVERRIDE" >&2
|
|
exit 2
|
|
fi
|
|
REPO_ROOT="$(cd "$REPO_OVERRIDE" && pwd)"
|
|
fi
|
|
|
|
resolve_dealer_number() {
|
|
local id=""
|
|
if [[ -n "$DEALER_OVERRIDE" ]]; then
|
|
printf '%s' "$DEALER_OVERRIDE"
|
|
return 0
|
|
fi
|
|
id="$(bf_detect_device_id || true)"
|
|
bf_dealer_number "$id"
|
|
}
|
|
|
|
print_report() {
|
|
local id="" dealer="" host="" wg_pub="" wg_path="" rustdesk=""
|
|
id="$(bf_detect_device_id || true)"
|
|
dealer="$(resolve_dealer_number || true)"
|
|
host="$(hostname 2>/dev/null || echo unknown)"
|
|
printf 'device\n'
|
|
printf ' device_id : %s\n' "${id:-unknown}"
|
|
printf ' dealer_number : %s\n' "${dealer:-unknown}"
|
|
printf ' hostname : %s\n' "$host"
|
|
printf 'wireguard\n'
|
|
if wg_pub="$(bf_wireguard_public_key)"; then
|
|
printf ' public_key : %s\n' "$wg_pub"
|
|
else
|
|
printf ' public_key : not present\n'
|
|
fi
|
|
if wg_path="$(bf_wireguard_private_key_path)"; then
|
|
printf ' private_key_path : %s (contents are never read or recorded)\n' "$wg_path"
|
|
else
|
|
printf ' private_key_path : not present\n'
|
|
fi
|
|
printf 'rustdesk\n'
|
|
if rustdesk="$(bf_rustdesk_id)"; then
|
|
printf ' id : %s\n' "$rustdesk"
|
|
else
|
|
printf ' id : not present\n'
|
|
fi
|
|
printf 'credential record\n'
|
|
printf ' path : %s\n' "$BF_CREDENTIALS_FILE"
|
|
if [[ -f "$BF_CREDENTIALS_FILE" ]]; then
|
|
printf ' present : yes (mode %s)\n' "$(bf_file_mode "$BF_CREDENTIALS_FILE")"
|
|
if bf_has_generated_password "$BF_CREDENTIALS_FILE"; then
|
|
printf ' administrator password recorded : yes (the value lives only in the file)\n'
|
|
else
|
|
printf ' administrator password recorded : no\n'
|
|
fi
|
|
else
|
|
printf ' present : no\n'
|
|
fi
|
|
}
|
|
|
|
# credentials_body <timestamp> <password> (stdout = the record file body)
|
|
# The only caller pipes this straight into bf_write_secret_file; the body is
|
|
# never printed anywhere else.
|
|
credentials_body() {
|
|
local ts="$1" password="$2"
|
|
local id="" dealer="" host="" wg_pub="" wg_path="" rustdesk=""
|
|
id="$(bf_detect_device_id || true)"
|
|
dealer="$(resolve_dealer_number || true)"
|
|
host="$(hostname 2>/dev/null || echo unknown)"
|
|
wg_pub="$(bf_wireguard_public_key || true)"
|
|
wg_path="$(bf_wireguard_private_key_path || true)"
|
|
rustdesk="$(bf_rustdesk_id || true)"
|
|
cat <<EOF
|
|
# BLUEFORCE DEVICE CREDENTIALS
|
|
#
|
|
# Generated by bf-creds on the device. Root-owned, mode 0600.
|
|
# Delivery target (bf-creds --fetch): <checkout>/${BF_SECRETS_SUBDIR}/<dealer-number>.txt
|
|
# That path is git-ignored and must never be committed.
|
|
#
|
|
# Remote access is NOT changed by this tool: OpenSSH password authentication
|
|
# stays disabled (PasswordAuthentication no) and the blueforce maintenance
|
|
# account stays locked. See docs/06-USERS-SSH-AND-PERMISSIONS.md.
|
|
|
|
device_id: ${id:-unknown}
|
|
dealer_number: ${dealer:-unknown}
|
|
hostname: ${host}
|
|
generated_at: ${ts}
|
|
generator: bf-creds
|
|
|
|
[wireguard]
|
|
public_key: ${wg_pub:-not-present}
|
|
private_key_path: ${wg_path:-not-present}
|
|
private_key: NOT RECORDED (the WireGuard private key never leaves the device)
|
|
|
|
[rustdesk]
|
|
id: ${rustdesk:-not-present}
|
|
|
|
[generated]
|
|
local_administrator_password: ${password}
|
|
note: recorded for delivery only; bf-creds does not apply this password to any
|
|
account, does not edit sshd_config, and does not open a password login path.
|
|
Applying it stays a deliberate operator action on the device console.
|
|
EOF
|
|
}
|
|
|
|
print_report
|
|
|
|
if [[ "$GENERATE" -eq 1 ]]; then
|
|
generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)"
|
|
password="$(bf_generate_secret)" || bf_die 'cannot generate a random secret (openssl and /dev/urandom are both unavailable)'
|
|
if ! credentials_body "$generated_at" "$password" | bf_write_secret_file "$BF_CREDENTIALS_FILE"; then
|
|
bf_die "could not write the credential record: $BF_CREDENTIALS_FILE"
|
|
fi
|
|
unset password
|
|
printf '\ncredential record written : %s (mode 600)\n' "$BF_CREDENTIALS_FILE"
|
|
printf ' the generated local administrator password exists in that file only;\n'
|
|
printf ' bf-creds does not apply it to an account, does not enable SSH password\n'
|
|
printf ' login, and leaves the blueforce account locked.\n'
|
|
else
|
|
printf '\ncredential generation : DISABLED (this is the default)\n'
|
|
printf ' no password was generated. Re-run with --generate (or BF_GENERATE_CREDENTIALS=1)\n'
|
|
printf ' to create %s.\n' "$BF_CREDENTIALS_FILE"
|
|
fi
|
|
|
|
if [[ "$FETCH" -eq 1 ]]; then
|
|
if [[ ! -s "$BF_CREDENTIALS_FILE" ]]; then
|
|
bf_die "--fetch needs an existing credential record: $BF_CREDENTIALS_FILE (run --generate first)"
|
|
fi
|
|
dealer="$(resolve_dealer_number || true)"
|
|
if [[ -z "$dealer" ]]; then
|
|
bf_die 'cannot determine the dealer number; pass --dealer-id <8 digits> (fail-closed: the delivery file name depends on it)'
|
|
fi
|
|
destination="$(bf_secrets_target_path "$REPO_ROOT" "$dealer")" || bf_die 'cannot build the delivery path'
|
|
if ! bf_deliver_secret "$BF_CREDENTIALS_FILE" "$destination"; then
|
|
bf_die "could not deliver the credential record to: $destination"
|
|
fi
|
|
printf '\ncredential record delivered : %s (mode 600, directory 700, git-ignored)\n' "$destination"
|
|
fi
|