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).
Components
Section titled “Components”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.
Hard constraints
Section titled “Hard constraints”- 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-requiredis not used at all.no-touch-requiredis not offered for client-generated keys, only relevant for enrolledsk-keys on the service path.
Invocation modes
Section titled “Invocation modes”| 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.
Certificate types
Section titled “Certificate types”- User – interactive SSH. Principals from OIDC claims + LDAP account identifiers.
- Service – non-interactive (scheduled jobs, file transfer, remote invocation). User-type certificate.
Service enrollment
Section titled “Service enrollment”- User invokes the client with requested options.
- 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).
- User authenticates via OIDC, reviews and adjusts options in the web UI, approves.
- Server returns an enrollment code bound to both the public key and the authorized option set.
- 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 | configssoossh host principals | mapping | checkssoossh service enroll | retrievessoossh cassh proxycommand– ensures a valid cert, then relays TCP. Requires an agent.host principals– implements sshd’sAuthorizedPrincipalsCommand. Called on every login attempt, runs as root, never touches the network; answers from the local mapping file, whichhost mappingedits.host check– read-only report, fromsshd -T, on whether the host will accept certificates. Exits non-zero on a failed check.ca– prints the CA public key forTrustedUserCAKeysand@cert-authoritylines.
Principal mapping
Section titled “Principal mapping”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.
pam_ssoossh checks
Section titled “pam_ssoossh checks”Generates an ephemeral keypair, requests a certificate, then verifies:
- signed by the expected CA
- public key in the certificate matches the key just sent
- principals authorize the local account being authenticated. The certificate
names the approver (their OIDC username plus the other accounts they hold);
the local
principals-mapdecides whether that is allowed to become this account, per “Principal mapping” above - 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.
Open questions
Section titled “Open questions”- 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.
Future
Section titled “Future”- Console login: the server half is built – a
consolecertificate 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_ifabove 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.
References
Section titled “References”- SSH certificate format: https://www.ietf.org/archive/id/draft-miller-ssh-cert-01.html
- SSH transport layer: https://www.rfc-editor.org/info/rfc4253/
- PAM spec: https://github.com/linux-pam/linux-pam/blob/master/doc/specs/rfc86.0.txt
- pocket-id: https://github.com/pocket-id/pocket-id
- lldap: https://github.com/lldap/lldap