{"slug": "show-hn-hermzner-provisioning-an-hardened-hermes-agent-on-hetzner-vps", "title": "Show HN: Hermzner – Provisioning an Hardened Hermes Agent on Hetzner VPS", "summary": "A developer released Hermzner, an open-source tool that provisions a hardened Hermes AI agent on a Hetzner VPS using rootless Podman and Tailscale. The setup includes Terraform and Ansible scripts for deployment, with security features like image digest pinning and firewall rules. It aims to provide a secure, disposable test environment for the Hermes agent before production use.", "body_md": "Provision a hardened Hermes Agent on Hetzner with rootless Podman and Tailscale.\n\n[Terraform](https://developer.hashicorp.com/terraform/install)>= 1.5[Ansible](https://docs.ansible.com/ansible/latest/installation_guide/)>= 2.15[Hetzner Cloud API token](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token)[Tailscale pre-auth key](https://tailscale.com/kb/1085/auth-keys)(reusable or ephemeral)\n\n```\n# 1. Copy and edit Terraform variables\ncp terraform/terraform.tfvars.example terraform/terraform.tfvars\nvim terraform/terraform.tfvars\n\n# 2. Copy and override Ansible defaults\nvim ansible/inventory/group_vars/all.yml\n# Required: set hermes_image_ref to a pinned digest\n#   Resolve the latest digest:\n#     curl -s \"https://hub.docker.com/v2/repositories/nousresearch/hermes-agent/tags/main\" | jq -r '.images[] | select(.architecture == \"amd64\" and .os == \"linux\") | .digest'\n#   Then set: hermes_image_ref: 'docker.io/nousresearch/hermes-agent@sha256:<digest>'\n\n# 3. Deploy\nHCLOUD_TOKEN=your_token TAILSCALE_AUTH_KEY=tskey-auth-... ./deploy.sh\n```\n\n`deploy.sh`\n\nruns Terraform (provisions VPS) then Ansible (configures it). Ansible connects via the server's **public IPv4** — Tailscale isn't available until the Tailscale role runs. Running `terraform plan`\n\nshows the diff between Terraform state and real infrastructure; this is normal behavior, not an error. `apply`\n\nreconciles them.\n\nUse this procedure for a first disposable test deployment. The goal is to validate Terraform, Ansible, Tailscale access, rootless Podman, and the Hermes runtime wiring before using a pinned production image.\n\nImportant:Run this only against a disposable Hetzner VPS. The smoke test may use`ALLOW_UNPINNED_IMAGE=true`\n\nfor convenience. Do not use this override for production.\n\nCreate and edit the Terraform variables file:\n\n```\ncp terraform/terraform.tfvars.example terraform/terraform.tfvars\nvim terraform/terraform.tfvars\n```\n\n| Component | Detail |\n|---|---|\n| VPS | Hetzner cx23, Ubuntu 24.04 |\n| Container Runtime | Rootless Podman (Quadlet default, Compose fallback) |\n| Network | Tailscale SSH + subnet access |\n| Service | Hermes Agent (gateway, API, optional dashboard) |\n| Mnemosyne Memory | SQLite-vec memory backend (optional, toggle via `hermes_mnemosyne_enabled` ) |\n| Backups | Daily local backups to /home/hermes/backups/; optionally encrypted with age |\n\n- Rootless container, all capabilities dropped, no-new-privileges\n- All ports bound to 127.0.0.1 (access via Tailscale SSH tunnel)\n- UFW default deny, only tailscale0 allowed\n- Read-only root filesystem, tmpfs for /tmp and /run\n- API key auto-generated, .env at 0600\n- Image digest pinning required (fail-closed if missing)\n\nSee [ SECURITY.md](/scicco/hermzner/blob/main/SECURITY.md) for the full security model, threat model, and design rationale.\n\n```\n# Access dashboard via SSH tunnel\nssh -L 9119:127.0.0.1:9119 hermes@<tailscale-ip>\n\n# Open http://127.0.0.1:9119 in browser\n```\n\nMnemosyne provides persistent memory (SQLite-vec) for the Hermes Agent, enabling long-term recall across conversations.\n\n```\n# ansible/inventory/group_vars/all.yml\nhermes_mnemosyne_enabled: true\n```\n\nWhen enabled, two dedicated Ansible roles handle the integration:\n\n— builds a custom container image extending the pinned Hermes base with`mnemosyne_build`\n\n`mnemosyne-memory[all]`\n\n, tags it as`localhost/hermes-mnemosyne:latest`\n\n— runs after the container starts: waits for the health endpoint, runs`mnemosyne_runtime`\n\n`mnemosyne.install`\n\ninside the container (plugin symlink + config.yaml update), and restarts the service only if changes were made\n\nThe Quadlet/Compose template uses the custom image and sets `MNEMOSYNE_DATA_DIR=/opt/data/mnemosyne`\n\nfor SQLite persistence.\n\nThe runtime install is automated by Ansible. The only manual step is selecting `mnemosyne`\n\nas the active memory provider:\n\n```\nssh hermes@<tailscale-ip>\npodman exec -it hermes /opt/hermes/.venv/bin/hermes memory setup\n# Select 'mnemosyne' from the provider list\n```\n\nVerify with `/opt/hermes/.venv/bin/hermes memory status`\n\n(inside container) — should show `Provider: mnemosyne`\n\n.\n\n```\nssh root@<tailscale-ip>\nsudo -u hermes XDG_RUNTIME_DIR=/run/user/$(id -u hermes) podman exec hermes python3 -m mnemosyne.install\nsudo -u hermes XDG_RUNTIME_DIR=/run/user/$(id -u hermes) systemctl --user restart hermes.service\nssh hermes@<tailscale-ip>\npodman exec -it hermes /opt/hermes/.venv/bin/hermes memory setup\n```\n\nMemory data lives at `/home/hermes/.hermes/mnemosyne/`\n\nand is included in daily backups.\n\n**Daily backups** run via cron at 2am (user `hermes`\n\n). They archive `/home/hermes/.hermes/`\n\n(data + auto-generated `.env`\n\n) to `/home/hermes/backups/`\n\nwith 30-day retention. When Mnemosyne is enabled, memory data at `/home/hermes/.hermes/mnemosyne/`\n\nis included automatically.\n\n```\n# Backup file format (plain):\n/home/hermes/backups/hermes-backup-20260521-020000.tar.gz\n\n# Backup file format (encrypted):\n/home/hermes/backups/hermes-backup-20260521-020000.tar.gz.age\n```\n\nEnable encryption by setting `backup_encryption_enabled: true`\n\nand `backup_age_recipient`\n\n(your age public key) in `group_vars/all.yml`\n\n.\n\n**Restore** from any backup archive to a running server:\n\n```\n# Plain backup:\n./scripts/restore.sh /path/to/hermes-backup-20260521-020000.tar.gz\n\n# Encrypted backup (requires age private key):\n./scripts/restore.sh /path/to/hermes-backup-20260521-020000.tar.gz.age --age-key ~/.age/key.txt\n```\n\nThe script auto-detects the Tailscale IP (falls back to `--tailscale-ip`\n\nif Terraform state is missing), copies the archive, stops the runtime, extracts, fixes permissions, restarts, and runs `verify.yml`\n\n.\n\n```\nterraform/       # Hetzner VPS provisioning\nansible/         # Server configuration (5 roles)\n  inventory/\n    group_vars/        # Ansible group variables (all.yml)\ndeploy.sh        # One-command deploy (auto-generates hosts.yml)\nteardown.sh      # Destroy everything\n```\n\n`scripts/repo_check.sh`\n\nruns local security and consistency checks against the repo. It scans for:\n\n- Secret leakage (API keys, tokens in committed files)\n- Dangerous container flags (\n`--privileged`\n\n, host networking, etc.) - Image pinning and port binding enforcement\n- Shell / YAML / Ansible syntax errors\n- Optional Terraform validation\n\n```\n./scripts/repo_check.sh\n```\n\nOutput is written to `hermzner-local-check-report.txt`\n\n(gitignored).\n\nSee `ansible/inventory/group_vars/all.yml`\n\nfor all configurable options, including feature toggles (`hermes_dashboard_enabled`\n\n, `hermes_mnemosyne_enabled`\n\n, `hermes_start_runtime`\n\n), resource limits, backup settings, and security policies.", "url": "https://wpnews.pro/news/show-hn-hermzner-provisioning-an-hardened-hermes-agent-on-hetzner-vps", "canonical_source": "https://github.com/scicco/hermzner", "published_at": "2026-06-18 23:15:40+00:00", "updated_at": "2026-06-18 23:30:45.843267+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-infrastructure", "ai-tools", "developer-tools"], "entities": ["Hetzner", "Tailscale", "Podman", "Terraform", "Ansible", "Hermes Agent", "Nous Research", "Mnemosyne"], "alternates": {"html": "https://wpnews.pro/news/show-hn-hermzner-provisioning-an-hardened-hermes-agent-on-hetzner-vps", "markdown": "https://wpnews.pro/news/show-hn-hermzner-provisioning-an-hardened-hermes-agent-on-hetzner-vps.md", "text": "https://wpnews.pro/news/show-hn-hermzner-provisioning-an-hardened-hermes-agent-on-hetzner-vps.txt", "jsonld": "https://wpnews.pro/news/show-hn-hermzner-provisioning-an-hardened-hermes-agent-on-hetzner-vps.jsonld"}}