Giving AI SSH Access

yakkomajuri2 pts0 comments

Giving AI SSH access – Yeri Tiete

I’ve been letting Claude (and previously Gravity) SSH into my fleet with minimal oversight, as whatever user it needed, often root. Which I guess is perfectly fine, until it isn’t… 😉

Last couple of weeks I’ve been trying to set up hardening for AI, but also the proper tooling access (via OrbStack, Claude Remote running on a separate Mac). I want AI to be able to test everything (locally) before pushing to prod (not always a given with Cloudflare Workers and Zero Trust for example) but also want to audit what it does.

As I was slowly adding the SSH key of my Claude user to all my servers, I started to hurt and feel… wrong. So it was time to update the purpose of my Box jumpbox; the Docker shell server I set up years ago and still use daily (over mosh) to reach all my servers and routers.

The agent wasn’t getting anything I didn’t already have (when I hadn’t added its SSH key to a particular device, I already told it to use box as a jump host at times, so it already had documented the use of a jump host).

The problem with SSHing, is that it was just doing it invisibly: leaving no papertrail. I mean, AI is amazing and I’ve never been able to make so much progress on stuff that was “forever in the backlog” as the last few weeks/months, but at the same time, it’s scary AF. Like giving an unreliable new employee the the codes to a nuclear bomb.

I wanted something easy: every command the agent runs (over SSH): captured, attributed, shipped (to Betterlogs, errrr, I mean BetterStack 😶‍🌫️), and searchable. And, it had to do this for its own user in box (no need to log my own sessions).

The setup

The trick is to stop sharing a login. I gave the agent its own user, alfons, with its own key in authorized_keys. My own logins stayed completely untouched (I only want to audit the AI, not myself).

Then sshd gets a ForceCommand, scoped to that user only:

Match User alfons<br>ForceCommand /usr/local/bin/ssh-audit<br>ssh-audit logs the proxied one-shot command ($SSH_ORIGINAL_COMMAND, e.g. ssh ocean systemctl status nginx), plus session open/close, then execs the real shell or command so nothing downstream changes. For the times the agent opens an actual interactive shell, a bash DEBUG trap in /etc/profile.d/box-audit.sh catches each command as it&rsquo;s typed.

Everything ships over OpenTelemetry to BetterLogs. A little box-log script builds an OTLP/HTTP payload with jq and POSTs it to a dedicated source. The curl is backgrounded, has a 3 second timeout, and every failure is swallowed. Audit is never allowed to hang or kill an SSH session. If BetterLogs stops working, I lose a (few) log line(s), but the AI can proceed.

And I of course removed the AI&rsquo;s SSH key from all the devices, forcing it to use the jump host.

How it flows

agent (morgoth) --ssh alfons@box "ssh ocean w"--><br>sshd → Match User alfons → ForceCommand /usr/local/bin/ssh-audit<br>→ ssh-audit: box-log proxied_command "ssh ocean w"<br>→ ssh-audit: exec bash -c "ssh ocean w"<br>→ box-log fires curl in background to BetterStack OTLP /v1/logs<br>→ command runs downstream as before, exit status unaffected<br>Morgoth is the dedicated Mac Mini m1 it now runs on. Ocean is an example server.

Each record carries box.user=alfons, the literal command, ssh.connection (source IP/port) and host.name — that last one set from the container&rsquo;s hostname so my two jumpboxes (box and boxnl) don&rsquo;t collapse into one indistinguishable stream.

Why not eBPF?

eBPF wants CAP_BPF; handing the jumpbox container kernel-level powers to audit itself is a worse trade than the thing I&rsquo;m trying to defend against.

The container runs with --network host, so an in-container eBPF agent would see every process on the host (including the ~20 unrelated containers next to it).

The AI here is brokered and trusted(ish), not actively trying to evade me. ForceCommand is enforced server-side and trivially captures the intent (ssh ocean w) instead of me reconstructing it from execve syscalls after the fact.

eBPF may be the right tool for fleet-wide runtime security, but not in this use case.

Prompt

The use of the jump host is doocumentend in the prompt. The SSH config only gives access to the jump host (and Gitlab, for pulls).

What&rsquo;s next…

I&rsquo;m considering adding a field where it needs to precede every command with a Gitlab issue ID and log that as well (so I can identify all commands it run for, for example, issue #123).

And potentially adding a list of blacklisted commands (ie rm -rf); not entirely sure yet how this would work, but worth pondering about.

#ai #ssh

© Yeri Tiete<br>Privacy<br>Stats

rsquo audit user host command agent

Related Articles