Skip to content

Design brief

SSO for SSH. Users authenticate via OIDC and receive SSH certificates instead of managing long-lived SSH keys. Self-hosted, homelab-friendly. Early development. Pronounced sue-sssh. Reference deployment uses pocket-id (OIDC) and lldap (LDAP).

ssoossh client – macOS, Linux, Windows. Go. Invoked from ssh_config. Talks to the server over HTTPS REST; waits for certificate issuance over SSE. Manages its own keypair, regenerated whenever a new certificate is needed. Loads results into ssh-agent, or writes private key / public key / certificate to disk when no agent is available. Config is YAML with command-line overrides.

ssoossh server – Go, gin. Authenticates users via OIDC, optionally enriches identity from LDAP. Decides certificate contents. Signs public keys. Serves a web UI for approval, confirmation, and per-user certificate history. The CA key comes from an inline PEM in the config or from a PKCS#11 token, behind one signing interface.

pam_ssoossh – Linux PAM module, auth group, sudo/su only. Generates an ephemeral keypair, requests a certificate, validates it, discards everything.

  • The server never receives private keys. The client sends private keys nowhere except the local ssh-agent or a local file.
  • The client never opens a listening port. No loopback redirect; the browser lands on the server and the client learns the outcome over its SSE stream.
  • Server config is the outer bound on every option. Client request asks, web UI narrows or adjusts, server config gates. Options the deployment does not permit are trimmed (not rejected) and shown in the web UI before approval.
  • Group membership never appears in a certificate. Groups feed the lifetime decision only.
  • verify-required is not used at all. no-touch-required is not offered for client-generated keys, only relevant for enrolled sk- keys on the service path.
Behaviour Match exec + ssoossh ssh login ProxyCommand
Client after issuance exits stays running, relays TCP for the session
ssh-agent optional required (ssh won’t re-read changed key files)
Key files on disk supported not supported

Certificate lifetime does not bound a session – the certificate is used once at session initiation. A valid certificate is reused until it expires, so a 10-hour certificate means one browser approval per workday, not one per session.

  • User – interactive SSH. Principals from OIDC claims + LDAP account identifiers.
  • Service – non-interactive (scheduled jobs, file transfer, remote invocation). User-type certificate.
  1. User invokes the client with requested options.
  2. Keypair is either supplied by the operator (BYO – server never sees the private half, may be HSM/PKCS#11/encrypted file) or generated by the client (optional passphrase).
  3. User authenticates via OIDC, reviews and adjusts options in the web UI, approves.
  4. Server returns an enrollment code bound to both the public key and the authorized option set.
  5. Later invocations post only the code. The server issues against the stored public key – the client never resubmits a key, so a stolen code cannot be paired with an attacker’s own keypair.
ssoossh ssh login | logout | proxycommand | inspect | config
ssoossh host principals | mapping | check
ssoossh service enroll | retrieve
ssoossh ca
  • ssh proxycommand – ensures a valid cert, then relays TCP. Requires an agent.
  • host principals – implements sshd’s AuthorizedPrincipalsCommand. Called on every login attempt, runs as root, never touches the network; answers from the local mapping file, which host mapping edits.
  • host check – read-only report, from sshd -T, on whether the host will accept certificates. Exits non-zero on a failed check.
  • ca – prints the CA public key for TrustedUserCAKeys and @cert-authority lines.

The certificate asserts identity; the host decides which local accounts that maps to. This is what keeps “userX may become root here” from being a statement about every host trusting the CA. The mapping is purely local to each machine: there is no server-side mapping and nothing syncs it down.

Certificate lifetime policy (design in progress)

Section titled “Certificate lifetime policy (design in progress)”

Server is the policy decision point, target sshd is the enforcement point. Server-side config only; default deny; matched rules intersect (shortest lifetime, narrowest principal set).

Signals: client source network; browser/client correlation (weak – source-address comparison can’t distinguish same host from same NAT, and the client’s self-report is forgeable, so it may shorten a lifetime when absent but never extend one when present); OIDC/LDAP group membership; certificate type and requested options.

source-address on user certs must handle NAT: union of the client’s own interface addresses and the address the server observed, capped by permitted zones. Client-supplied addresses are unverified input and need a policy ceiling.

Generates an ephemeral keypair, requests a certificate, then verifies:

  1. signed by the expected CA
  2. public key in the certificate matches the key just sent
  3. principals authorize the local account being authenticated. The certificate names the approver (their OIDC username plus the other accounts they hold); the local principals-map decides whether that is allowed to become this account, per “Principal mapping” above
  4. inside the validity window – tolerance is a module setting (clock skew is the real constraint; log observed skew on failure)

No nonce needed: the per-attempt keypair provides the freshness. Nothing is retained. AuthorizedPrincipalsCommand is irrelevant here (sshd directive), and pam_ssh_agent_auth is rejected because it requires agent forwarding.

  • Whether approval-prompt bypass should apply to the service path or user certs only.
  • Whether proof-of-possession (server challenge, client signs with the private key) is worth adding to service retrieve – a stolen code currently yields an unusable certificate but can still burn issuances and reveal the option set.
  • Which LDAP attributes become principals; narrow default vs. works-everywhere.
  • Console login: the server half is built – a console certificate type, POST /api/certs/console, the typed-code resolve endpoint, and the web UI that accepts a code. The module that drives it from a console is written separately in C, and it now draws the QR code as well. What is left is the deferred push channel; see Console login. Note the one correction that design made to the line this bullet used to carry: the per-host group restriction belongs in the host’s PAM stack (pam_succeed_if above the ssoossh line), not on the wire, because a field an unauthenticated caller sends can be omitted. What the server keeps is the disallow half, gated on the source address it observes rather than on a hostname the caller typed.