Files
esh-pfi-infrastructure/playbooks/esh-docker-vm-restic-drop-paperless-block.yaml
T
vh 466f7aa4e6 fix(elway): sudo uploads land root:root, validated and staged; add fleet ownership audit
elway's sudo upload did scp-as-user then `sudo mv`, and mv keeps the owner,
so every file it installed "as root" (systemd units, /etc configs, root-run
scripts) ended up owned by the SSH user. A sudoers drop-in installed that
way would be rejected by sudo outright.

elway:
- Sudo uploads now chown to root:root by default. Playbooks can override
  with `upload.owner:` and ad-hoc runs with `--owner`. An owner is refused
  on a non-sudo upload, and `--owner` outside an ad-hoc `--upload` is an
  error rather than silently ignored.
- Ownership and mode are applied to the STAGED file, then a single mv
  publishes it, so a failed chown can no longer leave the live path owned
  by the SSH user. chown runs before chmod so setuid bits survive. A trap
  removes the staged file on every exit path.
- A directory dest is refused before anything moves. mv would otherwise
  drop the file inside the directory under a staging name.
- `mode` was spliced unquoted into the remote root shell line. It is now
  validated as octal (^[0-7]{3,4}$) and shell-quoted, on both the sudo
  and non-sudo paths.
- `mode` and `owner` must be quoted YAML strings. YAML 1.1 turns a bare
  0644 into 420, a bare 1000:0 into 60000 and a bare yes into True, and
  str() of each is a different value that looks valid. All 85 playbooks
  already quote them.
- New preflight(): every step is resolved before any remote action. An
  undefined var or a templated owner that resolves badly is now refused
  up front, not after earlier steps have already changed the host.

scripts/fleet-ownership-audit.sh (new, read-only) finds files in root's
territory owned by a normal user. Tier A lists /etc, /usr/local, /root and
cron, judging symlinks by their target. Tier X covers files that root-run
units exec from /opt, /srv, /home or /volume1, reading drop-ins and quoted
paths. Tier B only summarises /opt app trees. A host that is unprivileged,
whose find fails, or whose run dies is reported INCOMPLETE (exit 5), never
clean. Completion is marked with a per-run nonce.

Verification: 24 unit tests in scripts/test_elway.py. Live checks on
esh-docker-vm: default lands root:root; the override lands infra-ops:adm at
4755 with setuid intact; a bad group fails with dest untouched and no
staging left; a directory dest is refused; a bad mode is refused before
any copy. Audit positive controls on nh3-dev (a drop-in-only root Exec, a
quoted path containing a space, a symlink to a user-owned target) were all
flagged; esh-docker-vm negative control was 0. Probes removed. Cross-model
bug-hunt (heid, Gróa arm + seat) findings folded.
2026-09-23 09:59:15 -07:00

63 lines
2.4 KiB
YAML

# esh-docker-vm: install the pre-backup hook without the paperless pg_dump
# block (operator decision 2026-09-23).
#
# paperless-ngx's Postgres is backed up at the source by esh-vm-db's
# fail-closed pg_dumpall. This host's second copy had been failing auth on a
# stale password for months behind a WARN. That block was the only consumer of
# /etc/restic/dbcreds.env, so the creds file is MOVED (not deleted) into the
# repair dir, out of the live config path.
#
# Rerunnable: the preserve/move steps are `creates:`-guarded; the chown step
# re-applies root ownership after every upload.
steps:
- name: Preserve the pre-change hook
sudo: true
shell: |
set -eu
install -d -m 0700 /var/lib/restic/repair-20260923
cp -p /etc/restic/pre-backup.sh /var/lib/restic/repair-20260923/pre-backup.sh.pre-paperless
creates: /var/lib/restic/repair-20260923/pre-backup.sh.pre-paperless
- name: Move the now-unused dbcreds.env out of /etc/restic
sudo: true
shell: mv /etc/restic/dbcreds.env /var/lib/restic/repair-20260923/dbcreds.env
creates: /var/lib/restic/repair-20260923/dbcreds.env
- name: Install the hook without the paperless block
sudo: true
upload:
src: configs/restic/esh-docker-vm/pre-backup.sh
dest: /etc/restic/pre-backup.sh
mode: '0700'
# Belt and braces: elway's sudo upload defaults to root:root since
# 2026-09-23 (before that it kept the SSH user's ownership). This hook is
# executed by root, so the verify below checks it either way.
- name: Make the hook root-owned
sudo: true
shell: chown root:root /etc/restic/pre-backup.sh /var/lib/restic/repair-20260923/pre-backup.sh.pre-paperless
verify:
- name: Hook parses under bash
sudo: true
shell: bash -n /etc/restic/pre-backup.sh
- name: No paperless dump or creds loader left in the live hook
sudo: true
shell: "! grep -q -E 'PAPERLESS_PG|dbcreds.env\"?$|pg_dump \\\\' /etc/restic/pre-backup.sh"
- name: dbcreds.env is gone from /etc/restic
sudo: true
shell: test ! -e /etc/restic/dbcreds.env
- name: Hook runs to completion with no WARN lines
sudo: true
shell: |
out=$(/etc/restic/pre-backup.sh 2>&1)
printf '%s\n' "$out" | grep -q 'stage ready:' && ! printf '%s\n' "$out" | grep -q 'WARN'
- name: Hook is root:root 0700
sudo: true
shell: test "$(stat -c '%U:%G %a' /etc/restic/pre-backup.sh)" = "root:root 700"