← All posts
Series BeaconSSH Series — Part 1 of 6

BeaconSSH: The Real Problem — Running SSH Commands On Behalf Of Users

·
beaconsshsshsecuritysystem-design

This is the first post in a series about BeaconSSH, a system I built for secure, identity-bound SSH access using short-lived certificates. Before diving into components or protocols, I want to explain the problem — because the problem itself is more interesting than the solution.

The Setup

I was building a web application that let users provision GitHub self-hosted runners on remote machines. The user would log in with their GitHub account, select a remote machine from a pool, and the backend would SSH into that machine to run setup commands — install the runner agent, configure it, register it with GitHub.

Simple on the surface. The constraints made it hard.

The Constraints

The backend needed to:

  • SSH into arbitrary remote machines — not the web server itself, separate infrastructure entirely
  • Execute setup and teardown commands
  • Act on behalf of the logged-in user

Under strict rules:

  • Users must not share passwords
  • Users must not share private SSH keys
  • The backend must not store long-lived credentials
  • Every action must be tied to a specific user identity
  • Access must be time-limited
  • Access must follow least-privilege

These aren’t aspirational goals. They’re hard requirements. If the backend stores a master SSH key that can access every host, a single compromise gives an attacker lateral movement across the entire fleet. If users share credentials, you lose auditability — you can’t tell who did what.

Why The Obvious Approaches Fail

“Just give the backend an SSH key.” This approach seems reasonable, but fails because it makes the backend a high-value target. A single private key that can reach every host is exactly the kind of credential that gets exfiltrated in breaches. You’d also lose identity — the backend connects as itself, not as the user.

“Have users upload their SSH keys.” Now you’re storing user private keys in your database. Even encrypted, this is a liability. Users reuse keys across systems. A breach of your key store potentially compromises their access to unrelated infrastructure.

“Use SSH agent forwarding.” This approach seems reasonable, but fails because agent forwarding is a well-documented security risk. Any process running as root on the intermediate hop can hijack the forwarded agent socket. Additionally, this requires the user to maintain an active SSH connection to the backend — which breaks the web application model entirely.

“Set up an SSH bastion/proxy.” Every connection flows through your infrastructure. You take on the operational burden of connection management and become a single point of failure. You’ve also centralized a surveillance point — every keystroke flows through your server.

None of these solve the fundamental problem: how do you grant time-limited, identity-bound SSH access to a remote machine without the backend holding persistent credentials?

The Answer: SSH Certificates

SSH certificates are a built-in feature of OpenSSH that almost nobody uses. A certificate authority signs a user’s public key, producing a certificate with an identity, a validity window, and constraints. The SSH host is configured to trust the CA’s public key. When the user connects, the host verifies the certificate against the CA — no authorized_keys entry needed.

The key insight: SSH certificates let you separate identity, authorization, and enforcement into different systems. The CA decides who gets access. The host enforces it. They don’t need to communicate at connection time.

What This Changed

With certificates, the flow becomes:

  1. User logs into the web app via GitHub OAuth
  2. Backend requests a certificate from the CA server with the user’s token and the target host
  3. CA validates the token, checks authorization policy, signs a short-lived certificate
  4. Backend uses the certificate to SSH into the target host
  5. The host validates the certificate against the trusted CA public key — no callback
  6. Certificate expires automatically

No passwords stored. No private keys shared. No persistent credentials. Each certificate is scoped to a specific user, specific principals, and a narrow time window.

What’s Next

The next post covers the full architecture — how the six components of BeaconSSH fit together and why each one exists.