#!/usr/bin/env bash # Install agent/host_agent.py as a systemd service on a monitored host. # # sudo ./deploy/install-agent.sh # loopback only (ssh tunnel in) # sudo ./deploy/install-agent.sh --bind 10.0.0.5 # serve the LAN, token required # sudo ./deploy/install-agent.sh --status # sudo ./deploy/install-agent.sh --uninstall # # The token is stored in a root-owned 0600 file and passed to the agent as --token-file, # never on the command line: argv is world-readable through `ps`, and this token guards # hostname, mounts, disk models, GPU model and process names. Re-running the installer # reuses the existing token — use --rotate-token when the old one leaked. # # The agent is read-only by construction (it answers GET and writes nothing), runs as its # own system user, and gets the usual systemd lockdown. It still needs to read /proc, /sys # and statvfs(), which the hardening below leaves alone. # # Disk health (`smartctl -i -A -H`, the `/disks` page) is the one add-on that needs more # than /proc and /sys: smartctl must open the disk device nodes. The installer therefore # adds the agent user to the `disk` group when one exists, and the unit device-allows the # disk nodes instead of PrivateDevices. This is raw *read* access to the disks — the # standard arrangement for SMART monitoring, granted deliberately — and its absence is a # documented degradation, never an install failure: no smartmontools (or no group) simply # means every disk reports `smart: null` with an explanatory error entry. set -euo pipefail SERVICE="dashboards-agent" AGENT_USER="dashboards-agent" AGENT_DIR="/usr/local/lib/dashboards-agent" AGENT_TARGET="$AGENT_DIR/host_agent.py" TOKEN_FILE="/etc/dashboards-agent.token" UNIT="/etc/systemd/system/$SERVICE.service" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" AGENT_SOURCE="$SCRIPT_DIR/../agent/host_agent.py" BIND="127.0.0.1" PORT="9120" TOKEN="${AGENT_TOKEN:-}" ROTATE=0 MODE="install" usage() { # the header comment *is* the help text, so it cannot drift from the flags awk 'NR > 1 { if (/^#/) { sub(/^# ?/, ""); print } else { exit } }' "$0" exit "${1:-0}" } while [ $# -gt 0 ]; do case "$1" in --bind) BIND="$2"; shift 2 ;; --port) PORT="$2"; shift 2 ;; --token) TOKEN="$2"; shift 2 ;; --rotate-token) ROTATE=1; shift ;; --status) MODE="status"; shift ;; --uninstall) MODE="uninstall"; shift ;; -h|--help) usage 0 ;; *) echo "unknown argument: $1" >&2; usage 2 ;; esac done need_root() { if [ "$(id -u)" -ne 0 ]; then echo "run as root: sudo $0 $*" >&2 exit 1 fi } status() { systemctl status "$SERVICE" --no-pager 2>&1 || true echo if command -v curl >/dev/null 2>&1; then printf 'health: ' curl -s --max-time 4 "http://$BIND:$PORT/health" || echo "(no answer)" echo fi } uninstall() { need_root "$0 --uninstall" systemctl disable --now "$SERVICE" 2>/dev/null || true rm -f "$UNIT" "$TOKEN_FILE" "$AGENT_TARGET" rmdir "$AGENT_DIR" 2>/dev/null || true systemctl daemon-reload echo "removed $SERVICE (the $AGENT_USER user was left in place; remove it with userdel)" } new_token() { # urlsafe and no padding: the token goes into HOST_TARGETS as URL userinfo # (`name=http://TOKEN@host:9120`), where '+' and '=' are annoying to quote. python3 -c 'import secrets; print(secrets.token_urlsafe(32).rstrip("="))' } install() { need_root "$0 --bind $BIND --port $PORT" [ -f "$AGENT_SOURCE" ] || { echo "no agent next to this script: $AGENT_SOURCE" >&2; exit 1; } if ! command -v python3 >/dev/null 2>&1; then echo "python3 is required on the monitored host (3.10 or newer)" >&2 exit 1 fi # The agent needs no privileges, so give it a user with nothing to lose. if ! id "$AGENT_USER" >/dev/null 2>&1; then useradd --system --home-dir / --shell /usr/sbin/nologin "$AGENT_USER" fi # SMART (phase 09): the usual distro answer for reading drive health is the `disk` # group — no sudoers entry, no root. Re-runs are idempotent. if getent group disk >/dev/null 2>&1; then if id -nG "$AGENT_USER" 2>/dev/null | tr ' ' '\n' | grep -qx disk; then echo " $AGENT_USER is already in the disk group (SMART reads enabled)" else usermod -aG disk "$AGENT_USER" echo " added $AGENT_USER to the disk group — raw device reads, which is what smartctl needs" fi else echo " no disk group on this host — SMART data will read null (harmless; see README)" fi if ! command -v smartctl >/dev/null 2>&1; then echo " note: smartctl is not installed — every disk reports smart: null until smartmontools is" fi install -d -m 0755 "$AGENT_DIR" install -m 0755 "$AGENT_SOURCE" "$AGENT_TARGET" if [ -f "$TOKEN_FILE" ] && [ "$ROTATE" -eq 0 ] && [ -z "$TOKEN" ]; then TOKEN="$(cat "$TOKEN_FILE")" echo "reusing the existing token in $TOKEN_FILE (--rotate-token for a new one)" elif [ -z "$TOKEN" ]; then TOKEN="$(new_token)" fi # 0600 root:root — the service runs as $AGENT_USER, and systemd (PID 1, root) reads the # file for it, so the agent process never needs read permission on it itself. (umask 0177; printf '%s\n' "$TOKEN" > "$TOKEN_FILE") chmod 0600 "$TOKEN_FILE" chown root:root "$TOKEN_FILE" cat > "$UNIT" <&2 journalctl -u "$SERVICE" -n 20 --no-pager >&2 exit 1 fi cat <