x‑hakt

A key with one job

A dashboard needs to peek at three other machines. Handing it my real SSH key would trade one small feature for the whole network. Here is the key I made instead.

The Mesh ssh · discovery updated Aug 30, 2026

authorized_keysforced-commandpermitopenleast-privilegeopenssh

The problem

Control Room is a dashboard that watches this mesh. One of the things it shows is which containers are up on the other hosts. To know that, it has to log in to each of them over SSH and ask.

The obvious way to make that work is to give it my own SSH key, mounted into the container read only. I tried that first. It worked immediately, which should have been the warning.

Think of it like a house-sitter. Someone is going to come in once a day and water the one plant on the porch. Do you hand them the whole keyring, the one that also opens the front door, the study, the safe? If they lose it, or someone lifts it from their bag, that is not “the plant got overwatered.” That is your house.

My SSH key is the whole keyring. It can become root on every box it touches. The job it is doing is “run docker ps and read four files.” A container breakout would have handed an attacker the entire mesh in exchange for a feature that lists container names.

my key, if it leaksthe scoped key, if it leakskeyroot @ lighthouseroot @ gp-formsroot @ podusaroot @ casparkeyone report
Same feature, two keys. The one on the left is what I nearly shipped.

The fix has three parts: the key can only run one script, that script can only reach out through one narrow tunnel, and the script itself only answers a fixed list of questions. Then I checked all three by trying to break them.

Building block 1: the key can only run one script

The key lives in the target host’s authorized_keys file, as one new line, with nothing removed:

restrict,command="/usr/local/bin/control-room-ro.sh" ssh-ed25519 AAAA... discovery

Think of a bank teller behind glass. You write what you want on a slip and push it through the slot. Whatever you wrote, they run one specific procedure. You cannot climb through the slot, you cannot walk around the counter, and you never see the vault.

restrict is the glass. It is an allowlist, not a blocklist: it turns off pty allocation, port forwarding, agent forwarding, X11, and ~/.ssh/rc, and it keeps turning off anything new that OpenSSH adds later. The forced command= is the one procedure. No matter what command the client sends, sshd runs that script instead. Control Room’s SSH call literally passes the string control-room-ro, and the server pays it no attention.

SSH client”anything”sshddiscards itcontrol-room-ro.shuname, df, docker ps, …
Whatever the client sends, sshd runs the pinned script. The request is not so much rejected as ignored.

Building block 2: one tunnel, one destination

One of the three hosts is not reachable directly. I get to it by jumping through another box, which SSH does with a ProxyJump. A ProxyJump needs to open a forwarded channel, and restrict just turned forwarding off.

Back to the teller: the glass has one small pass-through drawer, and it only reaches one desk in the back office. Not the whole back office. One desk.

That is permitopen. It re-enables forwarding to exactly one host and port and nothing else:

restrict,command="...",permitopen="10.10.10.x:22" ssh-ed25519 AAAA... discovery
control-roompermitopenjumpgp-formsany other host:porta local port on the jump✕ administratively prohibited
Forwarding is on for one hop only. Every other destination gets 'administratively prohibited'.

Building block 3: the script only answers a fixed list

The forced command is not a trusting script. SSH_ORIGINAL_COMMAND, the string the client asked for, is matched against exact literal patterns and never handed to a shell:

case "$SSH_ORIGINAL_COMMAND" in
  doc:/opt/gp-forms:SPEC.md) DOC=/opt/gp-forms/SPEC.md ;;
  # ...one line per file it is allowed to read...
  doc:*) echo "===REJECTED==="; exit 1 ;;
esac

Even the teller’s one procedure has a checklist taped to the counter. Ask for something that is not on it and the slip comes straight back.

Everything else the script does is a fixed sequence with no inputs: uname, nproc, free, df, the load average, docker ps -a, docker stats. The file reads are capped with head -c and refuse symlinks, so the script cannot be talked into being a general-purpose file reader or into returning something unbounded.

SSH_ORIGINAL_CMDcaseread-only action===REJECTED===
No pattern matches, no answer. The allowlist is a fixed switch in the script, not something the caller can extend.

I checked the wall by pushing on it

You do not trust a lock because the locksmith says so. You pull the handle. After switching the forced command over I tried the three things an attacker with this key would try:

  • Asked it to run id. It ran the pinned script instead and ignored me.
  • Asked for an interactive shell (ssh -tt). Rejected: PTY allocation request failed.
  • Opened a tunnel to a host that was not the permitted destination. Rejected, administratively prohibited. Then relayed an SSH banner through the one tunnel that is allowed and got bytes back, so the exception works and only the exception works.

Verified vs assumed

Verified: the three tests above, on the live hosts, after the change. Also that Control Room still gets its snapshot, in 2.4 to 2.9 seconds per host, cached five minutes.

Assumed, and worth another look: that the pinned script has no argument-handling path I missed, and that head -c 65536 is a sane cap rather than one I will regret when a STATUS.md grows. Neither has bitten yet.

The shape here is reusable. Any time something inside a container needs to reach a host outside it: a credential that exists for nothing else, cut down to the one capability the feature needs, one narrow exception if it truly needs one, and a test that proves the wall is where you think it is.

-x