Blueforce-Linux/admin/bf-creds
emirhanmamak-blueforce f8cdd1ed01 feat(admin): operator console, git-based install, optional credential delivery
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).
2026-09-16 15:26:17 +03:00

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