ADR-0007 · Plaintext Secrets + OS Hardening
We chose to store API keys and tokens as plaintext in ~/.hermes/.env on the Agent Host VPS, hardened at the OS level, rather than using a secrets manager or encrypted-at-rest solution.
Context
Section titled “Context”The Hermes Agent needs runtime access to multiple secrets: Telegram bot token, OpenRouter API key, Francois Personal Google Drive OAuth2 refresh token, GitHub fine-grained PATs scoped to specific fducat18 repos, and potentially more. These are stored in ~/.hermes/.env — a flat file that Hermes reads at startup. The day-one dashboard must not display or edit .env values. Djuly Google Drive, professional Google Drive, and professional GitHub tokens are explicitly out of day-one Agent Host scope.
Decision
Section titled “Decision”Plaintext .env with OS-level hardening (day one). Upgrade path documented below.
Use separate fine-grained personal GitHub PATs:
| Token | Scope | Permissions | Purpose |
|---|---|---|---|
HERMES_GITHUB_CODE_PAT | Selected personal fducat18 repos only | contents:write, pull_requests:write | Let Hermes edit personal code/wiki repos through branch-review delivery |
HERMES_BACKUP_GITHUB_PAT | Private hermes-backups repo only | contents:write | Push daily encrypted Hermes backup archives |
Avoid classic PATs, wildcard repo scope, professional org access, and long-lived tokens when a 90-day expiry is tolerable.
Security hardening (mandatory at install)
Section titled “Security hardening (mandatory at install)”- Dedicated user — Hermes runs as
hermesuser (not root). The.envfile is owned byhermes:hermeswithchmod 600. - SSH-key-only access — password and root login disabled on VPS. Only Francois’s ed25519 key can connect.
- UFW firewall — only port 22 (SSH) open inbound. All other traffic flows through Cloudflare Tunnel (outbound-only from VPS). Keeping SSH on 22 is acceptable because security comes from key-only auth, fail2ban, updates, and firewalling, not port obscurity.
- Dashboard binds to localhost — port 9119 listens on
127.0.0.1only. External access is via Cloudflare Tunnel + Access (email OTP). - No web secret editing day one — dashboard is read/operate only (sessions, logs, health, chat, non-secret operations).
.envdisplay/edit is done only over SSH from Bitwarden. - No secrets in git — the
.envfile is never committed. The repo contains only.env.examplewith placeholder values. - Telegram webhook via tunnel — not a public HTTP endpoint; delivered through the authenticated tunnel.
Threat model
Section titled “Threat model”| Threat | Impact | Mitigation | Residual risk |
|---|---|---|---|
| VPS root compromise | All secrets exposed | SSH-key-only, UFW, unattended-upgrades, fail2ban | Low — attacker needs your private key |
| Cloudflare Tunnel breach | Dashboard access to sessions, logs, health, chat, and non-secret operations | CF Access email OTP + dashboard on localhost only + no web secret editing day one | Low — requires compromising Cloudflare AND your email |
| Backup leak (VPS snapshot with .env) | Secrets in backup | Exclude ~/.hermes/.env from automated backups; re-provision secrets from Bitwarden if needed | Low — manual backup process |
| Log/stdout leak | Token appears in error log | Hermes redacts secrets in logs by default | Very low |
| Hostinger employee access | Theoretical hypervisor access | Accept provider trust (same as any VPS/cloud) | Accepted risk |
Why not encrypt at rest?
Section titled “Why not encrypt at rest?”- If someone has root, they can read the decryption key (it must be available at boot for the service to start)
- Adds complexity (systemd ExecStartPre decrypt step, key management)
- The real protection is preventing unauthorized access (SSH keys, firewall, tunnel), not encrypting something that must be decrypted to be used
Upgrade path (if threat model changes)
Section titled “Upgrade path (if threat model changes)”| Trigger | Upgrade to |
|---|---|
| Add Gmail/email access (highly sensitive) | Bitwarden Secrets Manager ($6/mo) — secrets fetched at runtime, never on disk |
| Add family members with VPS access | Per-user secret scoping + encrypted at rest with age |
| Regulatory requirement (client data) | Hashicorp Vault or Infisical (full audit trail, rotation, leases) |
What we sacrifice
Section titled “What we sacrifice”- No encryption at rest — if someone images the VPS disk, secrets are readable
- No audit trail — no log of “who accessed which secret when” (Bitwarden Secrets Manager would provide this)
- Manual rotation — changing a key means editing
.envand restarting the service
These tradeoffs are acceptable for a single-user personal estate with no client data and strong perimeter security.