#!/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. 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 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 <