A basic layer that is reasonable for a personal workstation exposed to the internet. Review it against your own needs.
- Tailscale only. UFW denies everything incoming except the
tailscale0interface. The public IP answers nothing, not even SSH. - SSH: keys only (no passwords), no root login, and only
adminanddevmay log in (/etc/ssh/sshd_config.d/00-agent-workstation.conf). - fail2ban watches SSH. It never bans your tailnet (100.64.0.0/10).
- Lockout protection: the installer stops before SSH hardening and before the firewall until you confirm that you can log in over Tailscale.
- Tailscale key expiry. Node keys expire after 180 days by default, and an expired key takes the machine off your tailnet: with the firewall on, that is a lockout. Disable key expiry for this machine in the admin console (machine menu, "Disable key expiry"). The installer prints the date.
- Your tailnet is the perimeter. Anyone with a device in your tailnet can reach port 22 (they still need an SSH key). If you share the tailnet with other people, restrict who reaches this machine with a Tailscale ACL, and keep two-factor on your Tailscale login.
- Provider firewall. If your VPS provider offers a network firewall, turn it on with no inbound rules: a second layer that holds even if UFW is misconfigured. Tailscale keeps working through outbound connections.
SSH only answers over Tailscale, so losing Tailscale (expired key, deleted machine) means no SSH. The way back in is your provider's web console: log in as root there (console logins are not SSH, so PermitRootLogin no does not apply; reset the root password from the provider's panel if you never set one), then tailscale up again, or fix whatever broke. Nothing in the kit disables the console.
| User | Purpose | sudo | SSH |
|---|---|---|---|
root |
Emergencies only, from your provider's web console | No | No |
admin |
You, for administration | Yes, with a password | Yes |
dev |
Your repos and the coding agents | No | Yes |
hermes |
Hermes Agent (optional) | No | No |
restic |
Local backups (optional). It can read everything and write only its repository | No | No |
devuses rootless Docker: its daemon runs asdev, so a container escape lands indev, not root. Docker's system daemon stays disabled, and nobody is in thedockergroup, which would amount to root. Published ports default to 127.0.0.1.- Agents run as
dev, so a misbehaving agent can damagedev's files, not the system.
- Claude Code runs in auto mode. A second model, a safety classifier, reviews each action instead of asking you. Anthropic reserves
bypassPermissions(no checks at all) for isolated containers or VMs without internet access (permission modes), and this machine is online with your GitHub and NaN credentials. Gentle AI'spermissionscomponent setsbypassPermissions;kit-guardrailsmoves it back to auto after every install and everygentle-ai sync. To keep bypass anyway, create~/.config/agent-workstation/allow-bypassasdev. - Deny rules apply in every mode, auto included: Gentle AI denies reading or editing secrets (
.env,.ssh, keys, credentials,gh's token) and destructive commands likerm -rf ~; the kit addsgit push --no-verifyandgh repo delete. - A
pre-pushhook in every repo blocks deletingmainon the remote. ci-local(tests plus GGA's review of the whole PR) is the gate before any merge, both for agents and forwt rm.claude-trustonly marks folders under~/workand~/trees, your own repos, as trusted.- engram's HTTP API for
devlistens on 127.0.0.1:7437 and answers reads with no authentication, because gentle-shell's memory package only speaks HTTP. The Hermes loopback guard (below) is what keeps the other agent user away from it;adminis you. Hermes's own engram uses a unix socket in its private runtime directory, off that port. - nan-gate runs in a strict systemd sandbox (dynamic user, read-only system, no capabilities, filtered system calls, loopback and HTTPS only;
systemd-analyze securityrates it 1.1). It holds the machine's NaN key only to add it to GGA's reviews, on its own local port (127.0.0.1:4881). systemd hands it the key as a credential that only the gate can read. Any local user could send requests to that port, so it spends your NaN quota, never more: the users on this machine already hold the key or cannot log in.
- Hermes runs as its own user, with no sudo, no SSH and no GitHub credentials.
- It reads only the repos you share with
repo-add --hermes. They are copies in/srv/shared/repos, refreshed hourly bydev, that Hermes can read but not write. It cannot see~dev. - Every command it wants to run needs your approval, and scheduled jobs cannot run commands.
- On this machine it can connect only to nan-gate (its model). A firewall rule for the
hermesuser rejects every other local port, sodev's local services (engram, development servers, Docker ports) are out of its reach even if a prompt injection asks for them. - It has its own copy of engram and gentle-ai, built in its own home: nothing from
devruns as Hermes. - Only your Telegram user id can talk to the bot.
- Moshi, the phone terminal, connects over SSH or Mosh through Tailscale, like your computer: nothing new opens on the machine. Mosh listens on UDP ports 60000 to 61000 only on the Tailscale interface, because that is all the firewall lets in.
moshi-hook, installed only if you runkit-moshi, is a closed-source daemon from getmoshi.app running asdev. It listens on 127.0.0.1:24543 (out of Hermes's reach) and keeps one WebSocket to Moshi's servers, through which your phone receives agent events and sends approvals. An event carries at most 200 characters of your prompt, 80 of the reply and 256 of the command to approve, with project name, model and context use. The kit disables its usage uploads and localhost port scanning. Its pairing secret lives in~dev/.config/moshi, mode 600.kit-moshi removetakes it out.- The "Easy Pair" QR (
kit-moshi connect) is a short-lived credential: whoever scans it gets the phone's SSH key added todev'sauthorized_keys, through Moshi's service. Treat it like a password while it is on screen. The by-hand alternative,kit-phone-key, never involves Moshi's servers.
- They are asked for by
kit-login, never put inkit.confor in this repo. - They are stored with mode 600, readable only by the user that needs them, and never passed on a command line, where other users could see them.
- Your email stays out of public repos:
kit-login githubsets git to commit as your GitHub noreply address, andrepo-add --newmakes a repo's first commit from this machine for the same reason. Commits that GitHub itself makes (a README created on the site, a merge done from the web or withgh pr merge) carry your account's real email unless you turn on "Keep my email addresses private" in GitHub's email settings. Do that once; it covers all your repos.
The kit installs software from other projects. Know where it comes from:
- Ubuntu, Docker, Tailscale, GitHub CLI, mise: their signed apt repositories.
- Claude Code and Hermes: their official install scripts, fetched over HTTPS and run as
devandhermes, never as root. - gentle-shell, engram, gentle-ai, GGA: built from the
mainbranch of their GitHub repositories, asdev(andhermesfor its own copy), every 3 hours. You get fixes fast, and you also get whatever lands onmain. If you would rather choose the moment, turn the automatic updates off (sudo kit-updates off) and run them when you want (sudo kit-updates now), or pin a version by editingbin/gentle-update. - Pi packages, CodeGraph, context7: from npm, as
dev. - Models: your prompts and code go to NaN, which states it keeps no logs of them, trains nothing on them and processes in the European Union (privacy policy), and to Anthropic when you use Claude Code. Read both providers' terms.
Nothing runs as root except the kit's own modules and the system services it configures.
- Ubuntu security updates install automatically. Reboots are up to you: the installer tells you when one is pending, and later
cat /var/run/reboot-requireddoes. - The Gentleman tools follow their
mainbranch and, unless you turned automatic updates off, refresh every 3 hours along with Claude Code, Herdr and Moshi's hook. The rest updates when you re-run the installer. Claude Code also updates itself on its own. - Local backups (optional) are encrypted with restic. Save the restic password somewhere else: without it they cannot be read. Since they live on the same disk, they also need your provider's snapshots to protect against losing the server.