thermograph/deploy/provision-agent-access.sh
Emi Griffith 0d8fc9f4d0 Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.

- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
  (not raw root) for agent-driven ops — passwordless sudo under a distinct
  username, sshd hardened to key-only auth, auditd logging every
  root-effective command. One line to revoke.

- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
  WireGuard tunnel rather than trusting the public internet with the
  overlay data plane, which Docker's own guidance says should never face it
  directly. Swarm ports locked to the tunnel interface once joined. This
  cluster's only workload is Forgejo — it does not orchestrate the
  Terraform-managed app deploys, so nothing here can strand the app's
  single-writer database.

- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
  Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
  Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
  auto-merge workflow step is dropped — it existed only to work around
  GitHub's paywalled branch protection on private free-tier repos, which
  Forgejo has no such tier for; native "auto merge when checks succeed"
  replaces it, and as a real git push (unlike GitHub's non-triggering
  token-merge) it fires the LAN deploy naturally with no double-trigger
  logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
  on Forgejo's default action registry); actions/checkout and
  actions/setup-python resolve unchanged.

Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.

One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00

90 lines
4.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# Provisions a dedicated, full-sudo login for an AI coding agent on this VPS.
# Idempotent — safe to re-run (e.g. to rotate the key or re-apply hardening
# after an OS upgrade). Run as root (or via an existing sudo-capable user):
#
# sudo bash /opt/thermograph/deploy/provision-agent-access.sh
#
# The agent's public key is embedded below (AGENT_PUBKEY) rather than passed
# as an argument, so this file alone is the whole bootstrap — copy the current
# one from the repo and run it; there's nothing else to fetch or fill in.
#
# What this does, and why it's a SEPARATE user rather than direct root login:
# - A distinct login name ("agent") gives a clean "who did this" audit trail
# that shared/root login destroys. It is still full-root-capable via
# passwordless sudo — this is a full-trust grant, just an attributable and
# independently revocable one.
# - Revocation is one line: delete AGENT_PUBKEY's line from
# /home/agent/.ssh/authorized_keys, or `deluser --remove-home agent` +
# `rm /etc/sudoers.d/agent`. Your own access is untouched either way.
#
# This script does NOT touch the app, Docker, Postgres, or Terraform-managed
# state — it only manages the "agent" login and sshd/auditd hardening.
set -euo pipefail
# --- fill this in when rotating the key; the current value is the one
# generated for this engagement --------------------------------------------
AGENT_PUBKEY="${AGENT_PUBKEY:-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICUwrp/neW5UJVmsK9SRveghJ+ayX3r59qgkL9Zo0mqO claude-agent@thermograph-infra}"
AGENT_USER="${AGENT_USER:-agent}"
if [ "$(id -u)" -ne 0 ]; then
echo "Run as root (sudo bash $0)." >&2
exit 1
fi
echo "==> User: $AGENT_USER"
if ! id "$AGENT_USER" >/dev/null 2>&1; then
adduser --disabled-password --gecos "" "$AGENT_USER"
fi
echo "==> Passwordless sudo (full, by design — see header comment)"
echo "$AGENT_USER ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/"$AGENT_USER"
chmod 440 /etc/sudoers.d/"$AGENT_USER"
visudo -cf /etc/sudoers.d/"$AGENT_USER" # fail loudly on a syntax error rather than silently locking sudo
echo "==> Authorized key"
install -d -m 700 -o "$AGENT_USER" -g "$AGENT_USER" /home/"$AGENT_USER"/.ssh
# Replace wholesale rather than append, so re-running after a key rotation
# doesn't accumulate stale keys.
echo "$AGENT_PUBKEY" > /home/"$AGENT_USER"/.ssh/authorized_keys
chown "$AGENT_USER":"$AGENT_USER" /home/"$AGENT_USER"/.ssh/authorized_keys
chmod 600 /home/"$AGENT_USER"/.ssh/authorized_keys
echo "==> sshd hardening (fleet-wide, not just for this account)"
SSHD_DROPIN=/etc/ssh/sshd_config.d/99-agent-hardening.conf
cat > "$SSHD_DROPIN" <<'EOF'
# Managed by deploy/provision-agent-access.sh — key-only login, no passwords.
# Root itself still can't password in; PermitRootLogin prohibit-password
# leaves direct root key-login available for a human who already has a key
# there, without opening a password path for anyone.
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
sshd -t # validate before reload; a bad config here can lock out the whole box
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
echo "==> auditd: log root-effective commands (survives shell history -c)"
if ! command -v auditctl >/dev/null 2>&1; then
apt-get update -y -q
apt-get install -y -q auditd audispd-plugins
fi
AUDIT_RULES=/etc/audit/rules.d/agent-root-cmds.rules
cat > "$AUDIT_RULES" <<'EOF'
# Every root-effective exec, tagged for `ausearch -k agentcmd`.
-a always,exit -F arch=b64 -S execve -F euid=0 -k agentcmd
-a always,exit -F arch=b32 -S execve -F euid=0 -k agentcmd
EOF
augenrules --load 2>/dev/null || true
systemctl enable --now auditd 2>/dev/null || true
echo
echo "Done. Verify from your own machine (not this box):"
echo " ssh -i <agent-private-key> $AGENT_USER@<this-host> sudo whoami # -> root"
echo " ssh -o PasswordAuthentication=no <any-user>@<this-host> # password path confirmed dead"
echo
echo "Revoke any time: delete the key line in"
echo " /home/$AGENT_USER/.ssh/authorized_keys"
echo "or remove the account entirely:"
echo " deluser --remove-home $AGENT_USER && rm -f /etc/sudoers.d/$AGENT_USER"