diff --git a/playbooks/bootstrap-infra-ops-user.yaml b/playbooks/bootstrap-infra-ops-user.yaml new file mode 100644 index 0000000..6f2a239 --- /dev/null +++ b/playbooks/bootstrap-infra-ops-user.yaml @@ -0,0 +1,77 @@ +# Bootstrap the `infra-ops` agent identity on a PFI-OWNED box. +# +# Creates a dedicated, key-authenticated, NOPASSWD-sudo system user so the +# infra-ops agent can complete DevOps work (apt, systemctl, service lifecycle) +# end-to-end instead of handing sudo steps back to the operator. +# +# WHY a dedicated identity (not the agent borrowing lkraven/vh sudo): +# - Clean audit trail — every agent action attributable to `infra-ops`, +# separate from human accounts, with sudo I/O logging on. +# - Revocation = pull one authorized_keys line + one sudoers file. +# - No human password ever in the agent's reach (key-gated NOPASSWD). +# +# RUN (operator, with YOUR sudo — infra-ops doesn't exist yet, so you bootstrap +# it as your normal account; elway prompts for your sudo password once, lazily +# on the first sudo step): +# scripts/elway --playbook playbooks/bootstrap-infra-ops-user.yaml +# Fleet (a lot of servers): +# scripts/bootstrap-infra-ops-fleet.sh +# +# SCOPE — PFI-owned Linux boxes ONLY. NEVER run on: +# - SureFire sf-* / sfsrv-ana (tenant property — coordinate, don't own) +# - corviduo-dev (Worldtree partner app-layer) +# - esh-* (ESH home lab — non-PFI) +# - nh3-nas (Synology DSM) (no standard useradd / sudoers.d) +# +# Idempotent: re-running reconciles the key + sudoers without error. + +vars: + ops_user: infra-ops + ops_pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN+1HBwfXrkfTYWdcnWCjLJ6VLAGC87gxH5h5vKaaA3c infra-ops@pfi-fleet" + # Optional authorized_keys from="..." source restriction. Empty = none. + # Hardening follow-up once per-path source IPs (LAN vs WG tunnel) are pinned. + ssh_from: "" + +steps: + - name: Create the infra-ops system user (home + bash shell) + sudo: true + shell: useradd -m -s /bin/bash {{ ops_user }} + when: "! id {{ ops_user }} >/dev/null 2>&1" + + - name: Add infra-ops to the docker group (only if docker is installed) + sudo: true + shell: getent group docker >/dev/null && usermod -aG docker {{ ops_user }} || echo "no docker group — skipped" + + - name: Ensure infra-ops ~/.ssh exists (700, owned) + sudo: true + shell: install -d -m 700 -o {{ ops_user }} -g {{ ops_user }} /home/{{ ops_user }}/.ssh + + - name: Install infra-ops authorized_keys (exact line, mode 600) + sudo: true + shell: | + line='{{ ops_pubkey }}' + [ -n '{{ ssh_from }}' ] && line='from="{{ ssh_from }}" {{ ops_pubkey }}' + printf '%s\n' "$line" > /home/{{ ops_user }}/.ssh/authorized_keys + chown {{ ops_user }}:{{ ops_user }} /home/{{ ops_user }}/.ssh/authorized_keys + chmod 600 /home/{{ ops_user }}/.ssh/authorized_keys + + - name: Install NOPASSWD sudoers + command logging (visudo-validated before install) + sudo: true + shell: | + f=/etc/sudoers.d/{{ ops_user }} + printf 'Defaults:%s log_output\n%s ALL=(ALL) NOPASSWD:ALL\n' '{{ ops_user }}' '{{ ops_user }}' > "$f.tmp" + chmod 440 "$f.tmp" + if visudo -cf "$f.tmp"; then mv "$f.tmp" "$f"; else rm -f "$f.tmp"; echo "VISUDO VALIDATION FAILED"; exit 1; fi + +verify: + - name: infra-ops user exists + shell: id {{ ops_user }} + changed_when: "false" + - name: authorized_keys present, mode 600, correct key + sudo: true + shell: test "$(stat -c %a /home/{{ ops_user }}/.ssh/authorized_keys)" = 600 && grep -q 'infra-ops@pfi-fleet' /home/{{ ops_user }}/.ssh/authorized_keys + changed_when: "false" + - name: sudoers file valid + NOPASSWD present + sudo: true + shell: visudo -cf /etc/sudoers.d/{{ ops_user }} && grep -q 'NOPASSWD:ALL' /etc/sudoers.d/{{ ops_user }} + changed_when: "false" diff --git a/scripts/bootstrap-infra-ops-fleet.sh b/scripts/bootstrap-infra-ops-fleet.sh new file mode 100755 index 0000000..a20f1bf --- /dev/null +++ b/scripts/bootstrap-infra-ops-fleet.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Fleet bootstrap of the `infra-ops` agent identity across PFI-OWNED boxes. +# Runs playbooks/bootstrap-infra-ops-user.yaml on each host via elway. +# +# RUN THIS YOURSELF (operator). Each host's first bootstrap needs YOUR sudo, so +# elway prompts for your sudo password once per host (lazy). After bootstrap the +# agent authenticates as `infra-ops` with its own key (NOPASSWD) and never needs +# your password again — re-runs are then promptless and idempotent. +# +# Usage: +# scripts/bootstrap-infra-ops-fleet.sh # tiers 1+2 (default) +# PFI_TIER=compute scripts/bootstrap-infra-ops-fleet.sh # tier 1 only +# PFI_TIER=all scripts/bootstrap-infra-ops-fleet.sh # + tier 3 sensitive infra +# scripts/bootstrap-infra-ops-fleet.sh irv-ml1 # explicit host(s) +# +# SCOPE — PFI-owned Linux boxes ONLY. This list deliberately EXCLUDES: +# SureFire sf-*/sfsrv-ana (tenant), corviduo-dev (partner app-layer), +# esh-* (non-PFI home lab), nh3-nas (Synology DSM — no useradd/sudoers.d). +# Hosts must resolve as ssh aliases (or be reachable as your account). + +set -uo pipefail +cd "$(dirname "$0")/.." + +PLAYBOOK=playbooks/bootstrap-infra-ops-user.yaml + +# Tier 1 — compute / docker hosts (where the agent recurs) +TIER1=(irv-ml1 ana-ml2 ana-docker nh3-docker) +# Tier 2 — PFI app VMs / LXCs +TIER2=(pfi-ana-webhost ana-filebot pfi-pteradactyl pfi-tacticalrmm ana-nas) +# Tier 3 — sensitive infra (DB, backup, network, hypervisors): opt-in via PFI_TIER=all +TIER3=(pfi-postgres pbs-ana pbs-nh3 ana-wg pfi-pve nh3-pve) + +if [ "$#" -gt 0 ]; then + HOSTS=("$@") +else + case "${PFI_TIER:-default}" in + compute) HOSTS=("${TIER1[@]}") ;; + all) HOSTS=("${TIER1[@]}" "${TIER2[@]}" "${TIER3[@]}") ;; + *) HOSTS=("${TIER1[@]}" "${TIER2[@]}") ;; + esac +fi + +echo "infra-ops fleet bootstrap → ${#HOSTS[@]} host(s):" +printf ' %s\n' "${HOSTS[@]}" +echo + +declare -a OK=() FAIL=() +for h in "${HOSTS[@]}"; do + echo "================ $h ================" + if scripts/elway "$h" --playbook "$PLAYBOOK"; then + OK+=("$h") + else + FAIL+=("$h") + echo "!! bootstrap FAILED on $h (continuing)" + fi + echo +done + +echo "================ summary ================" +echo "ok (${#OK[@]}): ${OK[*]:-none}" +echo "fail (${#FAIL[@]}): ${FAIL[*]:-none}" +[ "${#FAIL[@]}" -eq 0 ]