Skip to content

0004 — SSH: opkssh for people, the secret store's SSH CA for hosts

Status: Accepted; partly superseded by 0011 (machines and hosts move to the secret store); refined by 0015 Date: 2026-09-25

Context

sluisctl credential ssh already gets a person a short-lived certificate from a secret store's SSH CA — a key made on the caller's machine, signed once, handed to the agent (design/sluisctl.md#credential-the-broker-for-what-openbao-mints, connect/openbao.md). That is a courier in front of a certificate authority: correct under 0002, but a detour through this repository's own binary for something OpenPubkey SSH (opkssh) does directly — an OpenID Connect ID token, verified against an issuer's key set, admitted straight into sshd with no certificate authority and no broker in between.

Host identity is a different question from user identity: an SSH server needs a key nobody rotates by hand and a client that trusts it without asking on every connection, which is exactly what a certificate authority is for. The two problems should not share one answer.

Decision

People authenticate with opkssh. sluis is opkssh's OIDC issuer: a public client row declaring opkssh's loopback redirects (http://localhost:{3000,10001,11110}/login-callback), and server-side policy written as oidc:groups:<internal group> — the same groups claim every other relying party reads, bound directly rather than through a certificate. A server needs the opkssh binary installed and wired into sshd via AuthorizedKeysCommand (with the matching AuthorizedKeysCommandUser), network reach to the issuer's JWKS endpoint, and its own expiration policy set to 24h to match this issuer's session bound rather than opkssh's own default; nothing else. Session lifetime otherwise follows 0001's 24-hour absolute limit, the same as anywhere else a person's sign-in is the credential.

Hosts keep their key signed by the secret store's SSH CA, exactly as connect/openbao.md's host side already describes — a certificate authority is still the right tool for a key that must never be a person's problem to rotate, and clients trust it with @cert-authority the way they always have.

sluisctl credential ssh is removed once opkssh is adopted, per 0007: a courier in front of a certificate authority stops earning its keep once the direct path exists.

The blocker, stated plainly

opkssh cannot be adopted yet. It verifies only RS256, PS256, ES256 and EdDSA ID tokens today, and access-issuer signs ES384 by default (0005) — the one algorithm outside that list. Adoption waits on upstream OpenPubkey support for ES384, or on whichever alternative arrives first; sluisctl credential ssh is not removed, and stays the supported path for people, until one of those lands. An installation could work around the gap today by setting signingKey.certificate: {algorithm: RSA, size: 2048, encoding: PKCS1} (reference/configuration.md), but that is a key-rotation decision made for one relying party's benefit, not a default this repository asks for.

Consequences

Until the blocker clears, sluisctl credential ssh and opkssh are not both maintained in parallel as competing paths — the certificate broker stays the one documented way in for people, and this record is the tracked reason the direct path is not there yet. When ES384 support lands, the migration is: declare opkssh's client, write the oidc:groups: policy on each server, cut over, then remove the broker subcommand and its documentation, following the removal shape in not-served.md#what-was-removed-and-why.

Alternatives considered

Teleport (or a similar access-plane product) for SSH. Rejected: it is a second identity plane end to end — its own agents, its own certificate authority, its own notion of a session — where opkssh needs only an issuer this repository already runs and a claim it already mints.

Keep the secret store's SSH CA for people too, indefinitely, i.e. the current state, permanently rather than as an interim. Rejected as the long-term answer: it is one more hop (exchange, login, sign) for something a verified ID token can do directly, and it is why this record exists rather than leaving the courier undocumented as though it were the destination.

Tailscale SSH. Gives host reachability and identity together on a tailnet an installation already runs, but ties SSH authorization to tailnet membership rather than to this repository's own groups vocabulary — a second policy surface for the same decision opkssh makes from the token this issuer already mints. Worth revisiting for an installation whose hosts already live on a tailnet, but not the default here.