Skip to content

Support SSH user certificates (client-side cert auth + CA issuance) #209

@rado0x54

Description

@rado0x54

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

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions