Skip to content

Hosted SSH keys

The only legitimate reason to opt in is a target host you can’t upgrade:

  • The target runs OpenSSH < 8.4, so it can’t validate a webauthn-sk-ecdsa-sha2-nistp256@openssh.com signature at all.
  • You can’t change sshd_config on it (managed appliance, embedded device, frozen distro, vendor-shipped image).
  • You still want to drive the connection from a ShellWatch-managed broker for routing, audit, or pure-convenience reasons.

If you can update sshd on the target, do that instead — see Quick start → server requirements for the OpenSSH 8.4+ + PubkeyAcceptedAlgorithms config that makes a passkey endpoint work.

A passkey-credentialed connection and a file-key-credentialed connection are not equivalent from a security standpoint:

Passkey credential (recommended)File-based SSH key (this page)
/sign/:id approvalRequired, every connectionRequired, every connection
Credential locationOn the user’s authenticator (hardware / platform / synced)A long-lived .pem file on the broker host
Credential lifetimeEphemeral signature, fresh every connectionLong-lived; the on-disk key signs every handshake
If the broker host is compromisedAttacker can deny/delay sign requests, but cannot mint a signatureAttacker has the SSH credential outright
If the file leaks from keyDirectoryN/AEquivalent to handing out the SSH private key
WebAuthn UV bit on the SSH signatureYes (with PubkeyAuthOptions verify-required enforced)Not applicable — it’s a normal pubkey signature, no UV bit
Who can use itWhoever has the registered passkeyAdmin account only

The broker is now a production credential vault for any host whose authorized_keys lists one of these .pem keys. Treat the keyDirectory like the ~/.ssh/ of a privileged user.

ShellWatch’s KeyDirectoryWatcher discovers .pem files in the configured keyDirectory and surfaces them only inside the admin account. Non-admin accounts on the same broker:

  • Don’t see the file keys in any UI listing.
  • Can’t reference them when configuring or opening endpoints.

The discovered keys aren’t “assigned” to specific endpoints — they sit in the admin account as a key inventory. When the admin opens a session to a target, ssh2 presents the available credentials during the SSH handshake, and sshd accepts whichever one matches a line in ~/.ssh/authorized_keys on the target. (This is the standard OpenSSH key-matching flow, not a ShellWatch-specific binding.) The admin still has to put the public half of the key on the target’s authorized_keys for it to succeed — see Setup step 5 below.

That admin isolation is intentional: the people who can read raw .pem files off the broker disk are the same people who could already read them via shell access to the broker host. Exposing them to non-admin accounts would gain nothing and lose plenty.

If after reading the above you still want this:

Terminal window
mkdir -p keys
chmod 700 keys

The broker process should own this directory — chown it to whichever user the broker runs as before generating any keys.

Terminal window
ssh-keygen -t ed25519 -f ./keys/my-server.pem -C "shellwatch"
chmod 600 ./keys/my-server.pem

Use a fresh keypair — don’t reuse anything from your personal ~/.ssh/. The matching .pub will be written next to the .pem; it’s also viewable in the admin UI later (step 5).

3. Make keyDirectory reachable to the broker

Section titled “3. Make keyDirectory reachable to the broker”

Docker (docker-compose.yml) — add the volume to the service:

volumes:
- ./data:/app/data
- ./keys:/app/keys
- ./config.yaml:/app/config.yaml:ro

If you override the container UID, also chown -R <uid>:<gid> ./keys.

docker run — add -v "$PWD/keys":/app/keys to the run command.

In config.yaml:

keyDirectory: ./keys # or an absolute path; defaults to ./keys

KeyDirectoryWatcher auto-discovers .pem files on startup and watches for changes — no restart needed when you add or remove a key. Each key needs chmod 600.

5. Authorize the public key on each target

Section titled “5. Authorize the public key on each target”

Each .pem in your keyDirectory shows up in the admin UI (Settings → SSH keys) along with its OpenSSH-format public key. Use the Copy public key action to grab the ssh-ed25519 AAAA... shellwatch line, and append it to ~/.ssh/authorized_keys on every target you want to reach with that credential:

Terminal window
# On each remote target
echo "ssh-ed25519 AAAA... shellwatch" >> ~/.ssh/authorized_keys

That’s all the wiring. Opening a session to that target as the admin triggers a /sign/:id PendingAction; on approval, ssh2 presents the file key during the handshake and sshd accepts it via standard authorized_keys matching.

Before you commit to this path, double-check that:

  • The target really can’t be brought to OpenSSH 8.4+. (Even some “frozen” distros have OpenSSH backports.)
  • You can’t put a small jump host with a current sshd in front of the legacy target.
  • A non-WebAuthn sk-ecdsa (FIDO2 hardware-bound) key isn’t a workable middle ground for that target.

If any of those work, take them — you’ll get the full ShellWatch security model back.