Summary
ShellWatch currently authenticates outbound SSH connections with raw public keys only. We want to support SSH user certificates (*-cert-v01@openssh.com) — both as a client (ssh2 presenting a cert to a remote sshd) and as a CA (ShellWatch signing user keys to mint certs).
This unlocks two distinct product capabilities:
Use cases
-
Demo server (hosted ShellWatch).
A public ssh.shellwatch.ai box that new users land on during onboarding. Certs let ShellWatch hand each onboarded user a short-lived, principal-scoped credential without writing per-user state on the demo box (no AuthorizedKeysCommand callback, no key-registration API to harden). Demo sshd just trusts the ShellWatch CA — the demo box becomes stateless and cert TTLs handle revocation.
-
Self-hosted: admin-grade CA + policy engine.
In a self-hosted deployment the operator is the customer, so the trust closure stays internal. ShellWatch becomes the org's SSH CA: admins set up the CA, define principal taxonomies, attach policies (force-command, source-address restrictions, validity windows, per-action principals like inspect-only vs apply-changes), and onboard users by minting certs from their existing passkey-backed pubkey. Customer fleets add one line (TrustedUserCAKeys) and stop managing per-user authorized_keys. This is meaningfully better UX than the current model.
Trust-model constraint (hosted)
Hosted ShellWatch must NOT ask customers to unconditionally trust the ShellWatch CA on their own servers. That would grant the hosted operator effective root on customer fleets — a malicious or compromised operator could mint a cert binding any attacker-controlled key to any principal and walk in (the webauthn-sk-* cert variant does not mitigate this: the CA chooses what public key gets embedded, and the rpID-bound signature requirement only applies to legitimate cert holders).
For hosted, certs are only appropriate where ShellWatch is the relying party (the demo box, ShellWatch-managed targets). For customer-owned servers in hosted deployments, we continue using the direct passkey public key (webauthn-sk-ecdsa-sha2-nistp256@openssh.com) registered in authorized_keys — the customer's existing trust model, no third-party CA in the chain.
Self-hosted has no such constraint and gets the full CA story.
Current state of the ssh2 fork (rado0x54/ssh2#shellwatch)
Cert support is essentially absent on the client side. Specific gaps:
-
Cert body is not parsed. parseDER in lib/protocol/keyParser.js reads only the underlying pubkey material from a cert blob and discards everything else (nonce, serial, key_id, valid_principals, valid_after/before, critical_options, extensions, signature_key, signature). The library has no internal representation of a signed certificate.
-
authPK sends the wrong payload. Protocol.js authPK writes getPublicSSH() as the public key blob field of SSH_MSG_USERAUTH_REQUEST. For cert auth (per PROTOCOL.certkeys) that field must contain the full cert blob, not the bare pubkey. So even if we advertised algo ssh-ed25519-cert-v01@openssh.com, the server would reject the request as malformed.
-
No client API for cert + key. OpenSSH pairs id_ed25519 (private) with id_ed25519-cert.pub (cert). ssh2 has no equivalent — no cert: option on the connect config, no helper to combine them, no agent integration that returns a cert alongside its key.
-
sk-* cert keytypes aren't in the parser regex. The cert-suffix alternation covers ssh-(rsa|dss|ed25519) and ecdsa-sha2-nistp(256|384|521) only. webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com doesn't parse at all (even though the non-cert sk-* variant is supported via WebAuthnSKECDSAKey). This is the keytype combination we actually want for ShellWatch's passkey-anchored cert story, so it must be added.
Same gaps exist on upstream mscdex/ssh2.
Required work
In the ssh2 fork
- Parse the full OpenSSH cert structure into a typed object exposed alongside the pubkey.
- Plumb a
cert: option through the client config so callers can pass { privateKey, cert }.
- Fix
authPK to emit the cert blob (not the bare pubkey) when the keytype carries the cert suffix.
- Extend the cert-suffix regex to cover sk-* keytypes, particularly
webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com.
- Tests: round-trip cert auth against
OpenSSH server, including sk-* cert variant with rpID validation.
Bounded piece of work — protocol-level but not exotic. Estimate: a few hundred LoC + tests.
In ShellWatch
- CA key custody (HSM-backed in self-hosted prod; software-backed acceptable in dev).
- Cert issuance API: take a user pubkey + policy decision, return a signed cert with principals/TTL/extensions/critical-options.
- Principal taxonomy + policy model (admin-defined for self-hosted).
- KRL distribution path for revocation.
- Demo-box deployment:
TrustedUserCAKeys config, ForceCommand wrapper, log retention.
- Onboarding integration: mint a cert as part of the passkey-registration flow for users landing on the demo box.
Open questions
- Cert TTL defaults for the demo box (10 min? 1 hour? 24 hour?).
- Whether to expose CA-as-a-feature in hosted at all, or keep it self-hosted-only behind an admin gate.
- Whether to ship a "softer" intermediate option first:
AuthorizedKeysCommand + ShellWatch API for the demo box, deferring cert auth until the ssh2 fork is extended.
- Audit-log shape for cert issuance events.
Summary
ShellWatch currently authenticates outbound SSH connections with raw public keys only. We want to support SSH user certificates (
*-cert-v01@openssh.com) — both as a client (ssh2 presenting a cert to a remote sshd) and as a CA (ShellWatch signing user keys to mint certs).This unlocks two distinct product capabilities:
Use cases
Demo server (hosted ShellWatch).
A public
ssh.shellwatch.aibox that new users land on during onboarding. Certs let ShellWatch hand each onboarded user a short-lived, principal-scoped credential without writing per-user state on the demo box (noAuthorizedKeysCommandcallback, no key-registration API to harden). Demo sshd just trusts the ShellWatch CA — the demo box becomes stateless and cert TTLs handle revocation.Self-hosted: admin-grade CA + policy engine.
In a self-hosted deployment the operator is the customer, so the trust closure stays internal. ShellWatch becomes the org's SSH CA: admins set up the CA, define principal taxonomies, attach policies (force-command, source-address restrictions, validity windows, per-action principals like
inspect-onlyvsapply-changes), and onboard users by minting certs from their existing passkey-backed pubkey. Customer fleets add one line (TrustedUserCAKeys) and stop managing per-userauthorized_keys. This is meaningfully better UX than the current model.Trust-model constraint (hosted)
Hosted ShellWatch must NOT ask customers to unconditionally trust the ShellWatch CA on their own servers. That would grant the hosted operator effective root on customer fleets — a malicious or compromised operator could mint a cert binding any attacker-controlled key to any principal and walk in (the
webauthn-sk-*cert variant does not mitigate this: the CA chooses what public key gets embedded, and the rpID-bound signature requirement only applies to legitimate cert holders).For hosted, certs are only appropriate where ShellWatch is the relying party (the demo box, ShellWatch-managed targets). For customer-owned servers in hosted deployments, we continue using the direct passkey public key (
webauthn-sk-ecdsa-sha2-nistp256@openssh.com) registered inauthorized_keys— the customer's existing trust model, no third-party CA in the chain.Self-hosted has no such constraint and gets the full CA story.
Current state of the
ssh2fork (rado0x54/ssh2#shellwatch)Cert support is essentially absent on the client side. Specific gaps:
Cert body is not parsed.
parseDERinlib/protocol/keyParser.jsreads only the underlying pubkey material from a cert blob and discards everything else (nonce, serial, key_id, valid_principals, valid_after/before, critical_options, extensions, signature_key, signature). The library has no internal representation of a signed certificate.authPKsends the wrong payload.Protocol.jsauthPKwritesgetPublicSSH()as thepublic key blobfield ofSSH_MSG_USERAUTH_REQUEST. For cert auth (perPROTOCOL.certkeys) that field must contain the full cert blob, not the bare pubkey. So even if we advertised algossh-ed25519-cert-v01@openssh.com, the server would reject the request as malformed.No client API for cert + key. OpenSSH pairs
id_ed25519(private) withid_ed25519-cert.pub(cert). ssh2 has no equivalent — nocert:option on the connect config, no helper to combine them, no agent integration that returns a cert alongside its key.sk-* cert keytypes aren't in the parser regex. The cert-suffix alternation covers
ssh-(rsa|dss|ed25519)andecdsa-sha2-nistp(256|384|521)only.webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.comdoesn't parse at all (even though the non-cert sk-* variant is supported viaWebAuthnSKECDSAKey). This is the keytype combination we actually want for ShellWatch's passkey-anchored cert story, so it must be added.Same gaps exist on upstream
mscdex/ssh2.Required work
In the
ssh2forkcert:option through the client config so callers can pass{ privateKey, cert }.authPKto emit the cert blob (not the bare pubkey) when the keytype carries the cert suffix.webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com.OpenSSHserver, including sk-* cert variant with rpID validation.Bounded piece of work — protocol-level but not exotic. Estimate: a few hundred LoC + tests.
In ShellWatch
TrustedUserCAKeysconfig, ForceCommand wrapper, log retention.Open questions
AuthorizedKeysCommand+ ShellWatch API for the demo box, deferring cert auth until the ssh2 fork is extended.