0011 — SSH: people on opkssh, machines and hosts on the secret store's OpenBAO¶
Status: Accepted; amended by 0013 (the machine path is now sluisctl bao ssh -mode=ca / sluisctl bao write ... sign/<role>, not a dedicated sluisctl credential ssh); refined by 0015, 0016
Date: 2026-09-26
Context¶
0004 decided people would
move to opkssh — an OpenID Connect ID token verified straight into
sshd, no broker in between — once opkssh could verify this
installation's tokens, and that sluisctl credential ssh would be
removed "once opkssh is adopted", because a courier in front of a
certificate authority stops earning its keep once the direct path
exists. The blocker it recorded was concrete: opkssh's verifier accepts
RS256, PS256, ES256 and EdDSA ID tokens and refuses ES384, this
installation's default; the only way around it under 0004 was to move
the whole installation's default key off ES384, which 0004 declined to
ask of every other relying party for one relying party's benefit.
0009
(shipped v1.31.0) removes exactly that constraint: the issuer now signs
several algorithms at once, one key ring per algorithm, chosen per token
by the audience it is minted for. A client row can pin signing_alg:
RS256 or ES256 while every other audience keeps signing ES384. opkssh
can be adopted for people now, without moving the installation's default.
What 0004 did not separate is who was moving to opkssh. It says
"people authenticate with opkssh" in its Decision, but writes the removal
of sluisctl credential ssh as though nothing else used it. Something
else does: sluisctl credential ssh is also how a CI job or a
controller — a machine, holding a GitHub Actions OIDC token or a
Kubernetes ServiceAccount token, never a person at a browser — gets a
short-lived certificate today
(connect/openbao.md). opkssh's login is shaped
for an interactive OIDC sign-in, or, as of a recent opkssh release, for
GitHub Actions' own OIDC token specifically (opkssh login github). It
has no equivalent of a Kubernetes ServiceAccount token, or of this
issuer's service_account policy matchers more generally — the shape a
controller running in-cluster already authenticates with everywhere else
in this estate. Removing the broker on the schedule 0004 wrote would take
that population's only path away, not replace it with a direct one.
Decision¶
People authenticate with opkssh, exactly as 0004 decided, now that
the ES384 blocker is resolved: declare opkssh's client row with
signing_alg: RS256 or ES256 (never this installation's ES384
default, which opkssh cannot verify at all), write oidc:groups:<internal
group> policy on each server, and cut over. The mechanism is unchanged
from 0004's Decision; only the blocker under it is different.
sluisctl credential ssh stays — not as a courier for people, but as
the supported path for machines. A CI job or a controller exchanges its
own identity at this issuer, logs in to OpenBAO's JWT mount with the
result, and gets a short-lived signed SSH user certificate — the same
broker connect/openbao.md already documents for
database and client certificates, with SSH as one more kind it signs.
This reverses 0004's plan to remove the subcommand "once opkssh was
adopted": opkssh's adoption for people does not extend to
machines, because opkssh's provider model has no equivalent of a
Kubernetes ServiceAccount token or of an issuer that already knows how to
turn one into internal groups by service_account matcher. Removing the
broker would leave that population with no path rather than a more direct
one.
Hosts are unaffected and continue exactly as 0004 and
connect/openbao.md already describe: a host
proves itself to OpenBAO (the AWS auth method for a cloud VM; cert auth
or AppRole for bare metal) and is issued a host certificate from the SSH
CA, renewed by an OpenBAO Agent or a timer; clients trust the CA with one
@cert-authority line. Nothing here changes that.
The full shape for all three — the policy rows, the file formats, the commands — is connect/ssh.md, written alongside this record.
Consequences¶
sluisctl credential ssh's documentation stays live and is no longer
a stopgap. 0004 described it as the interim path for people; it is now
the permanent, documented path for machines, and
connect/ssh.md is where that is written down rather
than left to be inferred from 0004 having not yet been acted on.
opkssh's client row needs signing_alg pinned, and an installation
that forgets it is not gently caught. A row left at the installation
default signs ES384, which opkssh cannot verify at all; the failure
surfaces as an opkssh error on the server (unsupported signature
algorithm), not as anything this issuer refuses at sign-in — worth
saying plainly rather than discovering at the first person's login.
Machines keep one more hop than people: exchange, then an OpenBAO login, then a sign — versus opkssh's ID token verified directly. That extra hop is not new cost introduced by this decision: a job already takes it for a database or client certificate today, and this is the same shape applied to one more kind, not a second design.
A future opkssh release that added Kubernetes ServiceAccount tokens (or an equivalent workload-identity provider) as a supported provider would reopen this question — this record is the reasoning to revisit, not a permanent wall. Nothing here forecloses moving machines to opkssh later if that gap closes; it says why it has not moved them now.
Alternatives considered¶
opkssh for machines too, matching people exactly. Rejected for now:
GitHub Actions jobs are covered (opkssh login github, a provider opkssh
ships support for), but an in-cluster controller carrying a Kubernetes
ServiceAccount token is not — opkssh's policy language matches an issuer,
a subject and a claim, with no equivalent of this issuer's
service_account matchers (namespace, name, and which cluster's key set
signed it). Standardising every machine on opkssh today would mean either
leaving in-cluster callers with nothing, or maintaining a second,
narrower authorization surface for them on every host — the reason this
decision keeps one broker for every machine kind rather than splitting
machines further by how each happens to authenticate.
Teleport (or a similar access-plane product), as 0004 already weighed and this decision does not revisit differently: a second identity plane end to end — its own agents, its own certificate authority, its own session model — where the machine path here needs only the issuer and the secret store this estate already runs.
Tailscale SSH, likewise unchanged from 0004's answer: it ties SSH
authorization to tailnet membership, a second policy surface for the
same decision this issuer's groups already makes, and remains worth
revisiting only for an installation whose hosts already live on a
tailnet.