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.
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.
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.
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
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.
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