Skip to content

feat(auth): emit OCSF events for SSH session and sandbox token checks - #4762

Open
krishicks wants to merge 1 commit into
mainfrom
feat/2912-ssh-token-events/krishicks
Open

krishicks wants to merge 1 commit into
mainfrom
feat/2912-ssh-token-events/krishicks

Conversation

@krishicks

@krishicks krishicks commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The gateway now records OCSF security events for the credentials that request handlers check themselves, so they appear in the same audit stream as the router's authentication and authorization decisions:

  • Who: security operators following gateway authentication in a SIEM.
  • What:
    • SSH session tokens on ForwardTcp SSH connections emit an Authentication failure when rejected and a logon when first accepted, with protocol ssh_session_token.
    • IssueSandboxToken's runtime-identity check emits an Authentication failure.
    • The principal checks in IssueSandboxToken, RefreshSandboxToken, and GetCurrentUser, and the extension-service check in RefreshSandboxToken, emit authorization denials.
  • Why: these rejections previously reached only the caller and plain logs. An attacker guessing or replaying SSH session tokens, or a replaced sandbox runtime trying to mint a token, left no structured record.

Callers receive the same status codes and messages as before.

Related Issue

Closes #2912

This is the last PR for #2912. It covers the remaining authentication paths, which meets the criteria for reason categories across the gateway's supported authentication paths and for SSH bearer redaction. The operator setting for success events has been dropped from the issue for now: logons record each credential once an hour per replica, so they stay on.

Changes

  • SSH session tokens (acquire_forward_connection_guard):

    Rejection Reason
    No token on an SSH target missing_credential
    Token not found, or revoked invalid_credential
    Token minted for another sandbox audience_or_issuer_mismatch
    Token expired expired_credential
    Session store or record unreadable identity_provider_unavailable

    The router has already authenticated the caller's bearer token, so these events name that caller in user. An accepted token logs on once per replica through the shared logon record from feat(auth): emit OCSF logon events for accepted gateway credentials #4761, keyed on the caller and a digest of the token.

  • IssueSandboxToken: a compute-driver identity that doesn't match the sandbox's recorded runtime emits stale_identity. A sandbox with no record, or no usable recorded identity, emits unknown_identity; both use protocol compute_driver, and an unreadable store emits identity_provider_unavailable. A sandbox holding a gateway JWT is refused with method_not_permitted, which also raises the sandbox-authorization-boundary finding, since supervisors refresh instead.

  • RefreshSandboxToken: callers other than a gateway-JWT sandbox are refused with method_not_permitted. A request for an extension service the sandbox policy doesn't select is refused with the new extension_service_not_selected reason, naming the service as an extension_service resource. It raises no finding, because a policy update can race the supervisor's request.

  • Logons that wait for the handler. feat(auth): emit OCSF logon events for accepted gateway credentials #4761's router logs a credential on before the handler runs, but IssueSandboxToken and RefreshSandboxToken finish verifying the credential in the handler: the recorded runtime binding, and the token lineage. A stale runtime or a replayed, already-consumed bearer therefore recorded "succeeded" and then "failed" for one request. The router now skips the logon only for those deferred sandbox credentials (a compute-driver credential on IssueSandboxToken, a gateway JWT on RefreshSandboxToken), and each handler records it once its check passes. Users, operators, and gateway peers on these methods are fully verified by the router, so they still log on there before authorization refuses them. Moving the checks into the authenticators instead would have changed which error a misconfigured gateway returns.

  • GetCurrentUser: non-user principals are refused with method_not_permitted. The router already blocks these callers; this keeps the handler's own check audited.

  • auth::audit: emit_credential_failure and emit_ssh_session_logon take the request path and origin from the handler's request scope, as emit_authorization_denial does.

  • Docs: ocsf-json-export.mdx describes handler-checked credentials and when those two methods log on, adds the ssh_session_token protocol and the extension_service_not_selected reason, and notes that SSH session tokens log on even on gateways without authentication, since the token itself is verified.

Events never include presented tokens.

Testing

  • cargo nextest run -p openshell-server -p openshell-ocsf: all 2,613 tests pass.
  • cargo clippy -p openshell-server --all-targets -- -D warnings
  • Unit tests added or updated (written first; each failed for missing events before the change):
    • Each SSH token rejection emits its reason with the caller, operation, source address, and request ID, and never the token.
    • An accepted SSH token logs on once across two connections.
    • A mismatched compute runtime emits only stale_identity, with protocol compute_driver. A missing sandbox, or one with no recorded identity, emits unknown_identity.
    • A matching compute runtime logs on once, and a refresh logs its bearer on before a later denial. The router records no logon for those sandbox credentials, and a replayed refresh bearer records only stale_identity.
    • A user, operator, or gateway peer on either method still logs on at the router before its denial.
    • An SSH session token sent with a TCP target also logs on.
    • A gateway-JWT sandbox calling IssueSandboxToken, and a compute-driver principal calling RefreshSandboxToken, emit a denial and a finding.
    • A user calling RefreshSandboxToken emits a denial without a finding.
    • An unselected extension service emits extension_service_not_selected without a finding; a successful refresh emits nothing.
    • A sandbox calling GetCurrentUser emits a denial and a finding.
  • E2E tests added or updated: not applicable.

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Docs updated

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

Compatibility review (advisory)

New or updated breaking changes; see the full report for all findings.

1. api-behavior — No explicit risk acceptance found

The baseline emitted a status: Success Authentication event before deferred IssueSandboxToken runtime or RefreshSandboxToken lineage validation, while the candidate suppresses that event when the later validation fails.

  • Impact: SIEM rules relying on the documented first-acceptance success record or existing success-then-failure sequence will miss records and observe a different event sequence after a patch upgrade.
  • Action: Preserve the baseline success-event behavior for v0.1.6, or move the change to a minor train with notice and guidance for SIEM consumers to stop expecting the prior success-then-failure sequence.
  • PR: feat(auth): emit OCSF events for SSH session and sandbox token checks #4762
Review details
  • Execution: complete
  • Candidate: PR #4762 (88f4cc129de2f52e954379e238e68c488fb39906)
  • Baseline: 855e0e35a68d08d84580bc5a2702006319f71803 (855e0e35a68d08d84580bc5a2702006319f71803)
  • Reviewed: rust-sdk, python-sdk, go-sdk, typescript-sdk, api-behavior, cli, configuration, policy, helm, state

Advisory review; publication gates are unchanged.

Branch check run

Base automatically changed from feat/2912-authn-logon-events/krishicks to main October 20, 2026 21:58
@krishicks

krishicks commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

Acknowledging the compatibility finding and accepting the risk.

The baseline for this review is #4761 (855e0e3), which has merged to main but not shipped in a release. The success-then-failure sequence this PR removes was introduced there, so no released gateway has emitted it: v0.1.5 emits only authentication failures. No SIEM rule can depend on it yet.

The change is intended. A compute-driver credential on IssueSandboxToken, or a gateway JWT on RefreshSandboxToken, has not finished authenticating until the handler checks the recorded runtime or token lineage. Recording a success before that check, then a failure for the same request, would mislead consumers. Users, operators, and gateway peers on these methods still log on at the router as in #4761.

The remaining risk is a release cut before this PR merges, which would ship the interim sequence for one release. We'll merge this PR before the next release so it lands alongside #4761.

@krishicks
krishicks force-pushed the feat/2912-ssh-token-events/krishicks branch 2 times, most recently from b4aabdb to 6c87ace Compare October 20, 2026 22:56

@TaylorMutch TaylorMutch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at 6c87ace. No actionable findings. The deferred logon checks, credential redaction, and audit event coverage look sound.

Some credentials are checked by handlers rather than by the gateway's
authenticators, so their rejections only reached the caller and plain
logs. They now emit the same OCSF events as the router's checks:

- SSH session tokens. A ForwardTcp SSH connection whose token is
  missing, unknown, revoked, minted for another sandbox, or expired
  emits an Authentication failure with protocol ssh_session_token and a
  reason category. An accepted token logs on once, like other
  credentials. The router has already authenticated the caller, so these
  events name that caller.
- IssueSandboxToken. A compute-driver identity that no longer matches
  the sandbox's recorded runtime emits an Authentication failure
  (stale_identity); a sandbox with no record, or no usable recorded
  identity, emits unknown_identity. A sandbox calling it with a gateway
  JWT is refused with method_not_permitted and raises the sandbox
  boundary finding.
- RefreshSandboxToken. A caller that is not a gateway-JWT sandbox is
  refused with method_not_permitted. A request for an extension service
  the sandbox policy does not select is refused with the new
  extension_service_not_selected reason, without a finding, because a
  policy update can race the supervisor's request.
- Sandbox logons on IssueSandboxToken and RefreshSandboxToken. The
  router logged a compute-driver credential on IssueSandboxToken, or a
  gateway JWT on RefreshSandboxToken, before the handler finished
  checking the recorded runtime or the token lineage, so a credential
  that failed that check recorded a success and then a failure. Those
  handlers now record the logon once their check passes. Other
  credentials on these methods are fully verified by the router and
  still log on there, even when authorization then refuses them.
- GetCurrentUser. A non-user principal is refused with
  method_not_permitted. The router already blocks such callers, so this
  is defense in depth.

Callers receive the same status codes and messages as before, and the
events never include the tokens.

Refs #2912

Signed-off-by: Kris Hicks <khicks@nvidia.com>

@TaylorMutch TaylorMutch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the full diff and credential validation paths at 7892e4e. No actionable findings. Deferred logons occur after runtime or lineage validation, SSH session failures retain their existing caller statuses, and structured events avoid presented tokens. Tests cover rejection reasons, request correlation, deduplication, and boundary findings. Required CI gates pass; validation relied on code review and CI, with no local test run.

This branch has not been deployed

No deployments
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.

feat(auth): emit structured gateway authentication and authorization events

2 participants