Files
dashboards/deploy/agent-sudoers
T
ducoterra a08e693698 phase: 10_dev_direct_dial
**Phase 10 final pass — repo green; live criteria blocked off-repo.**

- Verified: `gateway` runs `dashboards-agent` (active/enabled, `10.1.1.27:9120` → `/health` 200, `/snapshot` 401 no-token / 200 bearer on-host, token matches `/etc/dashboards-agent.token`); `driveripper`/`deskwork` inactive, `sudo -n` → "a password is required", `:9120` empty.
- New measurement (transient runtime rule on gateway `firewall-cmd --add-port=9120/tcp`, **reverted** — runtime+permanent port lists verified back to the original set, no rich rules): laptop still gets no SYN through, while `:22`/`:80` on the same address answer in ms ⇒ the block is the lab gateway `10.2.0.1` port filter, not this app/host firewall/agent. No other live change made (no sudoers, no upsd, no UPS command).
- Tests/lint (exact): `bash .agents/validate.sh` → **EXIT 0** — `uv run pytest --cov --cov-fail-under=94` **793 passed, 11 skipped, coverage 98.33 %**; `ruff check src tests agent` clean; `pyright` 0 errors; `node --check` clean; hygiene clean; `uv pip list --outdated` empty (bumped fastapi 0.143.3→0.143.5, `uv.lock` only).
- `uv run pytest -m live --live` (`.env.dev` sourced): **4 passed, 1 skipped, 6 failed** — all six are `:9120`/`:3051` connect timeouts; zero parse/contract failures.
- Dev server restarted on current code: engines `ok`; 3×`host` + 3×`disks` + `nut` `DOWN` with real reasons (designed degradation, stub rows intact, no credential leakage).
- Completion criteria: screenshots ✓ (`data/logs/phase10-{index,system,disks,llm,ups}-live.png`); tunnel-vocabulary grep ✓ outside `.agents/` history (PLAN.md locked line, phase overviews, past reports; fixtures/`uv.lock` hits are digit substrings — `tests/test_deploy_scripts.py` guards operational files); no page-payload change ✓ (contract tests). **Not met, operator-side:** agents on all three hosts, `LIST UPS` from the laptop, `dev-server.sh status` all-ok, live suite green.
- Fixed in this pass: `sources/nut.py` unreachable-upsd message no longer prints a bare `(TimeoutError: )` (now `(timed out after 4s)` / `(ConnectionRefusedError: …)`), pinned by a new test + extended refused-port test.
- Docs: README verified consistent (every env var in `config.py` documented, routes/flags/`--with-llm`/pager sizes checked) — no changes needed. AGENTS.md fixed 4 inconsistencies: per-host sudo reality (gateway grants `NOPASSWD ALL`, which is why it installed without `agent-sudoers`; driveripper scoped; deskwork unrelated), the now-proven firewall finding, `/system` table typo ("agent per host"), gap-4 DB/WAL sizes + test count. `.env.example` `upsd.users` sample now one `instcmds` per line (it contradicted README and the in-app copy). `.env.dev` (gitignored) comment corrected to match the evidence.
- Flagged, untouched (locked): `.agents/PLAN.md` L92–95 still says upsd is "forwarded to `127.0.0.1:3494` in dev" — false after this phase; evidence + exact operator commands appended to `data/logs/NOTES-verify.md` §11.
- Next pending phase: `11_admin_login`.
2026-10-18 05:44:13 -04:00

77 lines
5.0 KiB
Plaintext

# sudoers grant that lets the unprivileged `ops` account install the homelab dashboards
# host agent on a monitored host. One line, one script path — deploy/nut-sudoers is the
# separate NUT grant on driveripper.
#
# Install (from the staged copy on the host, as root — the command-scoped `ops` sudo on
# these hosts does NOT include visudo, which is exactly why this file exists):
#
# sudo visudo -cf /home/ops/agent-install/deploy/agent-sudoers # syntax check FIRST
# sudo install -m 0440 -o root -g root /home/ops/agent-install/deploy/agent-sudoers \
# /etc/sudoers.d/dashboards-agent
# sudo visudo -cf /etc/sudoers.d/dashboards-agent # and check the installed copy
#
# Scope: an absolute path (no wildcards, `secure_path` is in effect), no shell, no editor,
# no `tee`, nothing that can amend this file. Running install-agent.sh without root prints
# this exact grant with the script's own resolved path, so a checkout at another path tells
# you what to add instead of saying "run as root" to someone who cannot.
#
# WHY THIS IS NEEDED
# The agent is read-only by construction: it answers GET /snapshot on a socket, writes
# nothing anywhere, and every request except /health needs the bearer token it reads from a
# file only root and the agent's own service user can read (0640 root:dashboards-agent — the
# agent opens the path itself, so the service user must be in that group). What needs root is
# the *install*, not the agent: creating the
# dashboards-agent system user, writing /usr/local/lib/dashboards-agent/host_agent.py,
# dropping /etc/systemd/system/dashboards-agent.service, storing the token, and running
# systemctl daemon-reload + enable --now (disable --now for --uninstall).
# Phase 10 runs each host's agent as that systemd service bound to the address the
# dashboard dials directly, so the account that deploys over ssh — ops — has to be able to
# run the installer, and that is the only reason it is granted anything.
#
# WHY THE WHOLE SCRIPT PATH AND NOT A FLAG LIST
# The flags carry no secrets: --bind/--port choose the socket, --status is read-only,
# --uninstall removes exactly what this install put in place, --rotate-token mints a token
# the operator is about to be shown anyway. sudoers can only match flags with a wildcard,
# and a wildcard list has to be edited in step with every flag the script gains — an edit
# whose failure mode is a broken install, not a smaller privilege. The one flag whose
# *argument* would be sensitive is --token, and no documented flow uses it: the installer
# generates the token, prints it once, and hands the agent --token-file, because argv is
# world-readable through ps while the file is not.
# ---- the grant ------------------------------------------------------------------ #
# The host has no repo checkout, so deploy/ and agent/ are staged to ~/agent-install/
# (phase 10 task 02 does the copy), which keeps this path identical on every host:
#
# ssh ops@host 'mkdir -p ~/agent-install'
# scp -r deploy agent ops@host:~/agent-install/
#
# `ops` is the fleet's monitored/deploy account; change only that field if the agent is
# installed by a different account. Nothing follows the path, which in sudoers means the
# script may be run with any of its own flags — see the block above.
ops ALL=(root) NOPASSWD: /home/ops/agent-install/deploy/install-agent.sh
# ---- making the grant mean something (one root command, recommended) ------------ #
# A sudo grant on a path is only as strong as who can write that path. ops owns
# ~/agent-install, so until the staged copy is root-owned this line is a convenience for a
# human, not a boundary. After staging, as root:
#
# sudo chown -R root:root /home/ops/agent-install
#
# ops can still wipe its own home directory and re-copy (it is ops' home), so this guards
# against drift and a stray edit — not against an operator who wants something else.
# ---- deliberately NOT granted --------------------------------------------------- #
# visudo, vim, nano, sh, bash, tee — no self-amendment of this grant, no shell, and no
# writing any file as root
# systemctl (anything else) — the installer already runs the few unit operations
# it needs as itself; a bare systemctl * could stop
# anything on the host
# cat /etc/dashboards-agent.token — the token is readable by root and the agent user
# only; install-agent.sh prints it once on first
# install and --rotate-token mints a new one
# useradd, usermod, groupmod — the installer creates its own user and its disk
# group membership; a bare usermod could add ops to
# any group on the box
# upscmd, upsrw — NUT instant commands stay the dashboard's gated
# POST /api/ups/command; see deploy/nut-sudoers