# Implement: Provision a Hardened Hermes Agent Base on an Ubuntu VPS

Inspect the target VPS, preserve its working access path, harden it in reversible stages, install Hermes as an unprivileged user, configure a model interactively, verify one CLI conversation, and report results. If you have tools, perform only the steps that are safe and observable, pausing for second-session checks, installer review, OAuth, or local credential entry. If guidance-only, return this workflow as an exact human-run procedure. Preserve unrelated state and ask only for required information that cannot be inspected.

## Outcome

Prepare an Ubuntu 22.04 or 24.04 VPS for Hermes without running the agent as root or creating a legacy `hermes.service`. Keep an existing non-root administrative SSH session usable, add a separate unprivileged Hermes account, enable a firewall and Fail2ban, add swap when appropriate, install Hermes from its current first-party installer, and complete one successful CLI interaction.

This base installation intentionally does not create an always-on gateway. Configure the supported `hermes-gateway` service only when adding Telegram or another messaging platform. Do not expose a dashboard or new inbound application port.

## Preconditions and inputs

Inspect or confirm:

- Ubuntu 22.04 or 24.04 with systemd, outbound HTTPS, and enough disk space.
- A working non-root sudo administrator session. Keep it open throughout the change.
- The effective SSH port and current UFW, SSH, swap, and user state.
- No requirement to preserve a legacy `hermes.service`; if one exists, stop and report it.

The user owns `<VPS_HOST>` and the public key for the Hermes login. A public key is not a credential, but validate its format before installing it. Provider credentials remain user-controlled secrets. Never request them in conversation or put them in commands; pause while the user completes OAuth or enters a credential in the interactive Hermes wizard.

## Safety constraints

- Never run Hermes as root or add the Hermes account to `sudo`.
- Do not overwrite `authorized_keys`; append a validated, non-duplicate public key.
- Do not assume SSH uses port 22. Derive the effective port with `sshd -T` before enabling UFW.
- Keep the original administrative session open. Validate SSH configuration and require a successful second key-authenticated session before disabling root or password login.
- If the second session or `sshd -t` fails, remove the new drop-in from the original session and reload SSH.
- Do not pipe downloaded content into a shell. Download the official installer, inspect its origin and contents, and require confirmation before execution.
- Do not display `~/.hermes/.env`, authentication files, provider credentials, or complete logs that may contain secrets.

## Implementation

1. Inspect the host without changing it:

   ```bash
   whoami
   . /etc/os-release && printf '%s %s\n' "$ID" "$VERSION_ID"
   free -h
   swapon --show
   sudo sshd -T | awk '$1 == "port" {print $2}'
   sudo ufw status verbose
   systemctl status fail2ban --no-pager
   systemctl status hermes.service --no-pager
   ```

   Continue only from a verified non-root sudo account. A missing Fail2ban or Hermes unit may return non-zero and is not itself an error. Stop if `hermes.service` is active.

2. Install operating-system prerequisites and create a 2 GiB swap file only when `/swapfile` is absent and no equivalent swap policy already exists:

   ```bash
   sudo apt update
   sudo apt upgrade
   sudo apt install python3 git curl ufw fail2ban openssh-server
   if [ ! -e /swapfile ]; then
     sudo fallocate -l 2G /swapfile
     sudo chmod 600 /swapfile
     sudo mkswap /swapfile
     sudo swapon /swapfile
   elif ! swapon --show=NAME --noheadings | grep -qw '/swapfile'; then
     sudo swapon /swapfile
   fi
   grep -q '^/swapfile ' /etc/fstab || printf '%s\n' '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
   ```

3. Create the fixed unprivileged `hermes` account and append its public key interactively. Accept only a key beginning with a recognized SSH public-key type; reject other input.

   ```bash
   id hermes >/dev/null 2>&1 || sudo adduser --disabled-password --gecos '' hermes
   read -r -p 'Hermes SSH public key: ' SSH_PUBLIC_KEY
   case "$SSH_PUBLIC_KEY" in ssh-ed25519\ *|ssh-rsa\ *|ecdsa-sha2-nistp*\ *) ;; *) printf '%s\n' 'Invalid public key' >&2; exit 1;; esac
   sudo install -d -o hermes -g hermes -m 700 /home/hermes/.ssh
   sudo touch /home/hermes/.ssh/authorized_keys
   sudo grep -qxF "$SSH_PUBLIC_KEY" /home/hermes/.ssh/authorized_keys || printf '%s\n' "$SSH_PUBLIC_KEY" | sudo tee -a /home/hermes/.ssh/authorized_keys >/dev/null
   sudo chown hermes:hermes /home/hermes/.ssh/authorized_keys
   sudo chmod 600 /home/hermes/.ssh/authorized_keys
   ```

4. Determine the effective SSH port, allow it before enabling UFW, and enable Fail2ban:

   ```bash
   SSH_PORT=$(sudo sshd -T | awk '$1 == "port" {print $2; exit}')
   case "$SSH_PORT" in ''|*[!0-9]*) printf '%s\n' 'Unable to determine SSH port' >&2; exit 1;; esac
   sudo ufw default deny incoming
   sudo ufw default allow outgoing
   sudo ufw allow "$SSH_PORT/tcp"
   sudo ufw --force enable
   sudo systemctl enable --now fail2ban
   ```

5. In a second terminal, verify key login to `<VPS_HOST>` for the Hermes account and reconfirm the original sudo administrator session. Only after both work, install and validate an SSH drop-in:

   ```bash
   printf '%s\n' 'PermitRootLogin no' 'PasswordAuthentication no' | sudo tee /etc/ssh/sshd_config.d/99-hermes-hardening.conf >/dev/null
   sudo sshd -t
   sudo sshd -T | awk '$1 == "permitrootlogin" || $1 == "passwordauthentication"'
   sudo systemctl reload sshd || sudo systemctl reload ssh
   ```

   Confirm the effective output reports `permitrootlogin no` and `passwordauthentication no`, then test another fresh key-authenticated session. Roll back with the original session by removing the drop-in, running `sudo sshd -t`, and reloading SSH if access fails.

6. Download the current official installer without executing it and inspect it:

   ```bash
   umask 077
   curl -fsSL https://hermes-agent.nousresearch.com/install.sh --output "$HOME/hermes-install.sh.review"
   less "$HOME/hermes-install.sh.review"
   ```

   Stop after inspection. Confirm the URL is the official Hermes domain and ask the user to approve this downloaded script. Only after that explicit approval, install and execute the reviewed file as the Hermes user:

   ```bash
   sudo install -o hermes -g hermes -m 700 "$HOME/hermes-install.sh.review" /home/hermes/hermes-install.sh
   sudo -iu hermes bash /home/hermes/hermes-install.sh
   sudo rm /home/hermes/hermes-install.sh
   rm "$HOME/hermes-install.sh.review"
   ```

7. If the installer did not already complete provider setup, run `hermes setup --portal` or `hermes model` in the Hermes user's interactive terminal; do not repeat a completed setup. Pause for OAuth or local credential entry. Then restrict the secret file if present; never print it.

   ```bash
   sudo -iu hermes hermes setup --portal
   if sudo test -f /home/hermes/.hermes/.env; then
     sudo chmod 600 /home/hermes/.hermes/.env
     sudo chown hermes:hermes /home/hermes/.hermes/.env
   fi
   ```

## Verification

Confirm these pass criteria:

1. A fresh key-authenticated administrative session works, root/password login is disabled, `sudo sshd -t` succeeds, and UFW allows the effective SSH port.
2. `swapon --show` reports the intended swap and Fail2ban is active.
3. `id hermes` shows no `sudo` group membership.
4. `sudo -iu hermes hermes --version` succeeds.
5. The user runs one normal Hermes CLI conversation and receives a model response.
6. No legacy `hermes.service`, public dashboard bind, or additional inbound port was created.

If a check fails, stop, preserve the working SSH session, and report the sanitized error plus the rollback or safest next diagnostic.

## Completion report

Return:

- Changes made: packages, account, key, swap, firewall, SSH drop-in, Hermes installation, and provider setup without secret values.
- Verification performed: commands and second-session checks with pass/fail results.
- User actions still required: OAuth, local credential entry, or CLI conversation not completed by available tools.
- Unresolved risks or blockers: access issues, legacy services, unsupported host state, or none.

### Sources

- Source article: [My Hermes Journey: Running a Self-Hosted Agent on Hetzner VPS](https://vgagaleski.com/blogs/my-hermes-journey-self-hosted-hetzner/)
- Official reference: [Hermes Agent Quickstart](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/getting-started/quickstart.md)
- Official reference: [Hermes Agent Messaging Gateway](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/)
- Official reference: [Ubuntu OpenSSH server guide](https://ubuntu.com/server/docs/how-to/security/openssh-server/)
- Official reference: [Ubuntu sshd manual](https://manpages.ubuntu.com/manpages/jammy/man8/sshd.8.html)
