Files
OpenShell/crates/openshell-server
krishicks 321b93a898 feat(auth): emit OCSF events for SSH session and sandbox token checks (#4762)
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>
2026-10-21 14:32:35 +00:00
..

OpenShell gateway server

The server owns control-plane RPCs, authentication, provider credential delivery, and sandbox lifecycle orchestration. The openshell-gateway crate composes the server with selected compute drivers.

Sandbox launch identity

A new start, including an automatic restart, commits a fresh runtime generation and an incremented authorization epoch before invoking the compute driver. The launch session ID is derived from the durable sandbox ID and generation. Retries reuse that generation and session, including after gateway restart. Credential renewal retains both; credentials for an older launch cannot authorize the new one.

Operator credential export

GetProviderCredentials exports selected profile-declared runtime environment keys from one provider in an explicitly named workspace. It requires the distinct Principal::Operator, established only from a verified TLS leaf certificate with OU=operator when mtls_auth.operator_enabled is enabled. The TLS listener supplies its certificate fingerprint separately from user identity; the ordinary identity-only serving path cannot establish an operator.

Operators explicitly inherit user/platform-admin permissions, but not sandbox or peer identities. Descriptor authorization and a handler guard both protect the export RPC. Operator mutation admission uses a separate caller namespace from ordinary mTLS users with the same certificate subject. Credential responses never enter durable mutation replay or generic interceptor routing.

The export resolver validates the entire selection before minting, resolves only selected credential handles, and fences provider revisions, refresh epochs, and profile declarations before delivery. Driver expiration and provider expiration combine using the earlier timestamp. OAuth minting inputs, broker-only keys, and supervisor-bound dynamic grants are not exportable. A failed response can still have committed refresh effects. Missing refresh-owned stored outputs trigger refresh-on-read; missing static outputs and other credential-backend errors fail closed without falling back to inline values.

Refresh coordination

Explicit rotation, background refresh, and operator refresh-on-read share the same refresh coordination:

  • A process-local weak keyed mutex coalesces waiters without retaining deleted providers indefinitely.
  • A bounded admission queue and executor pool limit detached refresh work.
  • PostgreSQL deployments also take a dedicated per-provider/per-credential advisory lock; SQLite uses the local mutex. Worker secret cleanup takes the same locks and reloads state before changing its resource version.
  • A durable refresh_in_progress marker precedes upstream minting. A refresh_committing marker protects the interval between grant persistence and provider-handle persistence. Only a completed provider commit publishes refreshed state, with a unique completed-mint identity in metadata annotations. Waiters coalesce against that identity, not bookkeeping changes to the resource version.
  • Automatic callers recheck live recovery and retry deadlines under both locks. A failed mint blocks queued automatic exchanges without disabling scheduled worker retries or a separately requested manual rotation.
  • Detached refresh tasks complete despite request cancellation, preserving replacement refresh tokens returned by the issuer.
  • Existing generation/CAS fencing remains active for deletion and reconfiguration. An ambiguous rotating-token exchange is parked for recovery rather than automatically consuming the grant again.

A crash or lost database-lock session can leave a parked marker. Reconfigure or reauthorize the refresh grant after investigating its issuer state. Changing a marker directly is not a supported recovery workflow.

Focused verification

cargo test -p openshell-server --features test-support,prebuilt-z3
cargo clippy -p openshell-server --all-targets --features test-support,prebuilt-z3 -- -D warnings

The ignored postgres_refresh_locks_coordinate_independent_replica_sessions test runs under mise run test:rust:postgres (see TESTING.md) and still accepts OPENSHELL_REFRESH_TEST_DATABASE_URL pointing to a disposable PostgreSQL database. It exercises independent sessions without the local mutex. Do not point this fixture at an existing deployment database.