Hosted SSH keys
When this exists
Section titled “When this exists”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.comsignature at all. - You can’t change
sshd_configon 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.
What you give up
Section titled “What you give up”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 approval | Required, every connection | Required, every connection |
| Credential location | On the user’s authenticator (hardware / platform / synced) | A long-lived .pem file on the broker host |
| Credential lifetime | Ephemeral signature, fresh every connection | Long-lived; the on-disk key signs every handshake |
| If the broker host is compromised | Attacker can deny/delay sign requests, but cannot mint a signature | Attacker has the SSH credential outright |
If the file leaks from keyDirectory | N/A | Equivalent to handing out the SSH private key |
| WebAuthn UV bit on the SSH signature | Yes (with PubkeyAuthOptions verify-required enforced) | Not applicable — it’s a normal pubkey signature, no UV bit |
| Who can use it | Whoever has the registered passkey | Admin 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.
Admin-only by design
Section titled “Admin-only by design”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:
1. Create the directory and lock it down
Section titled “1. Create the directory and lock it down”mkdir -p keyschmod 700 keysThe broker process should own this directory — chown it to whichever user the broker runs as before generating any keys.
2. Generate (or copy in) the SSH key
Section titled “2. Generate (or copy in) the SSH key”ssh-keygen -t ed25519 -f ./keys/my-server.pem -C "shellwatch"chmod 600 ./keys/my-server.pemUse 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:roIf you override the container UID, also chown -R <uid>:<gid> ./keys.
docker run — add -v "$PWD/keys":/app/keys to the run command.
4. Configure keyDirectory
Section titled “4. Configure keyDirectory”In config.yaml:
keyDirectory: ./keys # or an absolute path; defaults to ./keysKeyDirectoryWatcher 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:
# On each remote targetecho "ssh-ed25519 AAAA... shellwatch" >> ~/.ssh/authorized_keysThat’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.
Reconsider passkeys
Section titled “Reconsider passkeys”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
sshdin 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.
Where to next
Section titled “Where to next”- Self-hosting overview — the recommended (passkey-only) path.
- Hardening checklist — file permissions, systemd hardening, IP allowlist.
- Concepts → Security model — what the passkey path guarantees that file-based-key connections don’t.