Skip to content

fix: Validate SSH certificate that it doesn't contain more privilege than requested#361

Open
minhtule wants to merge 6 commits into
masterfrom
feat/mt/validate-vault-ssh-certificate
Open

fix: Validate SSH certificate that it doesn't contain more privilege than requested#361
minhtule wants to merge 6 commits into
masterfrom
feat/mt/validate-vault-ssh-certificate

Conversation

@minhtule

Copy link
Copy Markdown
Contributor

Related Tickets & Documents

Changes

  • Verify a CA-issued SSH certificate grants no more than the signing request before use, and reject it otherwise.

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.80%. Comparing base (7a35cdd) to head (e82024f).
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #361      +/-   ##
==========================================
+ Coverage   86.18%   86.80%   +0.62%     
==========================================
  Files          40       40              
  Lines        2830     2865      +35     
==========================================
+ Hits         2439     2487      +48     
+ Misses        265      261       -4     
+ Partials      126      117       -9     
Flag Coverage Δ
integration 54.53% <30.55%> (-0.44%) ⬇️
unit 80.27% <100.00%> (+1.72%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
internal/sshhandler/ca.go 91.81% <100.00%> (+12.40%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@minhtule minhtule changed the title Validate SSH certificate that it doesn't contain more privilege than requested fix: Validate SSH certificate that it doesn't contain more privilege than requested Jun 30, 2026
@minhtule minhtule requested a review from Copilot June 30, 2026 21:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens the SSH certificate signing flow by validating that a CA-issued SSH certificate does not exceed the privileges requested by the gateway, aligning with Issue #360’s requirement to distrust potentially misconfigured/compromised external CAs.

Changes:

  • Adds verifyCertificate and enforces it on Vault-signed certificates before use.
  • Validates certificate type, bound public key, principals, validity, and requested permissions/attributes.
  • Adds targeted unit tests covering policy-violation and Vault signing error scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
internal/sshhandler/ca.go Parses Vault-issued certs and enforces post-signing policy validation via verifyCertificate.
internal/sshhandler/ca_test.go Adds tests for Vault signing error paths and comprehensive verifyCertificate policy checks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/sshhandler/ca.go
Comment on lines +325 to +328
maxValidBefore := mustUint64(time.Now().Add(req.ttl).Add(clockSkewBuffer))
if cert.ValidBefore > maxValidBefore {
return fmt.Errorf("%w: validity %d exceeds requested TTL (max %d)", errCertPolicyViolation, cert.ValidBefore, maxValidBefore)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TTL is anchored at when the gateway receives and starts using the cert (it's used immediately on return), not when the request was sent. Treating "valid for ttl from when the cert becomes usable" as the intended meaning is reasonable, and it's also what Vault does (ValidBefore = signing_time + ttl). Under that reading the response delay doesn't grant extra lifetime, so I'll leave the check as-is

Comment thread internal/sshhandler/ca.go
Comment thread internal/sshhandler/ca.go Outdated
@minhtule minhtule force-pushed the feat/mt/validate-vault-ssh-certificate branch from 0cf0329 to e2833f7 Compare June 30, 2026 22:31
@minhtule minhtule requested a review from Copilot June 30, 2026 22:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread internal/sshhandler/ca.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread internal/sshhandler/ca.go
Comment on lines +325 to +328
maxValidBefore := mustUint64(time.Now().Add(req.ttl).Add(clockSkewBuffer))
if cert.ValidBefore > maxValidBefore {
return fmt.Errorf("%w: validity %d exceeds requested TTL (max %d)", errCertPolicyViolation, cert.ValidBefore, maxValidBefore)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve the cert renewal logic in #362 so backdating ValidAfter doesn't cause any issue. Enforcing a lower bound is kind of arbitrary so not adding this validation.

Comment on lines +538 to +543
{name: "validity exceeds TTL", req: userReq, setupFn: func(c *ssh.Certificate) {
c.ValidBefore = mustUint64(now.Add(2 * time.Hour))
}, wantErrMsg: "exceeds requested TTL"},
{name: "no expiry", req: userReq, setupFn: func(c *ssh.Certificate) {
c.ValidBefore = ssh.CertTimeInfinity
}, wantErrMsg: "exceeds requested TTL"},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve the cert renewal logic in #362 so backdating ValidAfter doesn't cause any issue. Enforcing a lower bound is kind of arbitrary so not adding this validation.

@minhtule minhtule force-pushed the feat/mt/validate-vault-ssh-certificate branch from d3c348b to 42aefa0 Compare June 30, 2026 23:41
require.NotErrorIs(t, err, errVaultSignFailed)
}

func TestVerifyCertificate(t *testing.T) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only this test is needed for the code change. The other tests for Vault CA (which didn't have any test!) is to improve code coverage.

@minhtule minhtule requested a review from clement0010 July 1, 2026 01:11

@clement0010 clement0010 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix & merge! One question: see below

Comment thread internal/sshhandler/ca_test.go Outdated
Comment thread internal/sshhandler/ca.go
Comment on lines +343 to +347
for opt := range cert.CriticalOptions {
if _, ok := req.permissions.CriticalOptions[opt]; !ok {
return fmt.Errorf("%w: unexpected critical option %q", errCertPolicyViolation, opt)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If CriticalOptions is restrictive, having extra critical options should technically be fine? 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true in general but force-command is the exception. It substitutes what runs on the session rather than narrowing it, so a CA injecting it (we currently request zero critical options) would silently change behavior. This is a fail-closed check on the CA doing exactly what we asked, so I'd keep the exact-match and reject anything unrequested.

Comment thread internal/sshhandler/ca_test.go Outdated
Comment thread internal/sshhandler/ca_test.go Outdated
@minhtule minhtule requested a review from sghiocel July 1, 2026 18:49
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.

3 participants