Skip to content

feat(handler): recognise SSH user certificates in PrivateKey secrets - #2007

Open
yellmean wants to merge 1 commit into
jumpserver:devfrom
yellmean:feat/ssh-cert-auth
Open

feat(handler): recognise SSH user certificates in PrivateKey secrets#2007
yellmean wants to merge 1 commit into
jumpserver:devfrom
yellmean:feat/ssh-cert-auth

Conversation

@yellmean

Copy link
Copy Markdown

feat(handler): recognise SSH user certificates in PrivateKey secrets

Summary

pkg/handler.buildSSHClientOptions currently calls
gossh.ParsePrivateKey on account.Secret and silently discards any
OpenSSH certificate line that follows the PEM block. This means a
certificate signed by an external CA (such as step-ca, Vault SSH, or
ssh-keygen -s) bundled in the same secret is never presented during
user-auth, and the SSH user-auth packet reaches sshd as a plain
publickey request instead of an *-cert-v01@openssh.com request.

This PR introduces a small internal package pkg/sshcert that detects
the trailing certificate line, validates that its public key matches
the embedded private key, and wraps both with ssh.NewCertSigner when
present. The ParsePrivateKey call in buildSSHClientOptions is
swapped for sshcert.NewSigner: blobs without a matching certificate
line fall back to the existing plain-key behaviour, so the change is
fully backwards-compatible for any existing deployment whose
account.Secret is a plain OpenSSH private key block.

The PR is deliberately agnostic about the certificate source — the
secret blob can be produced by step-ca, ssh-keygen, Vault SSH secrets
engine, or any other CA. The companion documentation in
docs/ssh-certificate.md describes the supported wire format and
references the external tools, without binding koko to any particular
one.

Diff stat

 docs/ssh-certificate.md   |  64 +++++++++++
 pkg/handler/server_ssh.go |  10 +-
 pkg/sshcert/cert.go       | 127 ++++++++++++++++++++++
 pkg/sshcert/cert_test.go  | 154 +++++++++++++++++++++++++++
 4 files changed, 354 insertions(+), 1 deletion(-)

The runtime change in pkg/handler/server_ssh.go is one function call
(gossh.ParsePrivateKeysshcert.NewSigner) plus one import line.

Compatibility

Scenario Behaviour
Old deployment, plain key in account.Secret Unchanged: sshcert.NewSigner returns the plain signer
New deployment, key + matching certificate in account.Secret CertSigner used, target host verifies via TrustedUserCAKeys
Mismatched cert (cert key != private key) sshcert.ErrCertMismatch, fail closed, no auth attempted

Tests

pkg/sshcert/cert_test.go covers plain-key fallback, valid cert
wrapping, key/cert mismatch, and Parse() metadata (serial,
principals). The runtime change in pkg/handler/server_ssh.go is
covered by the package's existing tests; no change to those is
required.

Documentation

docs/ssh-certificate.md describes the package, its entry points,
the supported wire format, and links to the relevant OpenSSH and
step-ca references. The PR does not add a step-ca integration
client, since that is a separate concern from the parsing capability.

Why this PR

Several operators deploy JumpServer alongside an external SSH CA
(smallstep step-ca being the most common) to issue short-lived SSH
user certificates against their asset fleet. With this PR, koko
recognises the resulting account.Secret payload and authenticates
with the certificate, with zero new dependencies and no change
to the Luna UI, the asset-account schema, or the JMS API
.

This PR does not endorse or integrate any specific CA. It only adds
the parsing capability that lets koko USE a certificate whenever
one is present in the secret blob. Operators can fill the secret

@yellmean
yellmean force-pushed the feat/ssh-cert-auth branch from 26cee69 to 7a4e280 Compare July 16, 2026 02:09
pkg/handler.buildSSHClientOptions currently calls
gossh.ParsePrivateKey on account.Secret and silently discards
any OpenSSH certificate line that follows the PEM block. This
means a certificate signed by an external CA (such as step-ca,
Vault SSH, or ssh-keygen -s) bundled in the same secret is
never presented during user-auth, and the SSH user-auth packet
reaches sshd as a plain publickey request instead of an
*-cert-v01@openssh.com request.

Introduce pkg/sshcert, a small helper package that detects the
trailing certificate line, validates that its public key matches
the embedded private key, and wraps both with ssh.NewCertSigner
when present. Swap the ParsePrivateKey call in
buildSSHClientOptions for sshcert.NewSigner: blobs without a
matching certificate line fall back to the existing plain-key
behaviour, so the change is fully backwards-compatible for any
existing deployment whose account.Secret is a plain OpenSSH
private key block.

The PR is deliberately agnostic about the certificate source -
the secret blob can be produced by step-ca, ssh-keygen, Vault
SSH secrets engine, or any other CA. The companion documentation
in docs/ssh-certificate.md describes the supported wire format
and references the external tools, without binding koko to any
particular one.

Tests in pkg/sshcert/cert_test.go cover plain-key fallback,
certificate wrapping, key/cert mismatch (ErrCertMismatch), and
the Parse() metadata path.
@yellmean
yellmean force-pushed the feat/ssh-cert-auth branch from 7a4e280 to b5da3e1 Compare July 16, 2026 02:42
@sonarqubecloud

Copy link
Copy Markdown

@LeeEirc

LeeEirc commented Jul 22, 2026

Copy link
Copy Markdown
Member

This format is not a common OpenSSH private key format. In theory, saving the account private key as “private key + certificate line” may fail, because the current API/backend validation usually only accepts standard private key content that can be parsed by ssh.ParsePrivateKey, and there is no separate logic to store or recognize the certificate.

In other words, even if Koko supports parsing a trailing certificate line, the combined format may not be persisted if Core/API already validates the private key format when saving the account secret.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants