Skip to content

fix(security): add SSH session token expiry, connection limits, and lifecycle cleanup#182

Merged
johntmyers merged 2 commits intomainfrom
fix/security-22-ssh-session-expiry
Mar 9, 2026
Merged

fix(security): add SSH session token expiry, connection limits, and lifecycle cleanup#182
johntmyers merged 2 commits intomainfrom
fix/security-22-ssh-session-expiry

Conversation

@johntmyers
Copy link
Copy Markdown
Collaborator

@johntmyers johntmyers commented Mar 9, 2026

🔧 security-fix-agent

Closes #22

Security Fix

Summary

SSH session tokens previously had no TTL and remained valid indefinitely until explicitly revoked. This change adds configurable token expiry, enforces concurrent connection limits at two levels, cleans up sessions on sandbox deletion, and runs a background reaper for expired/revoked sessions.

Limits Imposed

Limit Value Configurable Rationale
Token TTL 24 hours Yes (ssh_session_ttl_secs, 0 = disable) Bounds the window a leaked token can be exploited. 24h is safe because every CLI command creates a fresh token — no path reuses old tokens.
Per-token concurrent connections 3 No (compile-time constant) Prevents a single leaked token from being used to open parallel tunnels. Normal usage is 1 connection per token, so 3 provides headroom without enabling abuse.
Per-sandbox concurrent connections 20 No (compile-time constant) Prevents token proliferation from bypassing the per-token limit. Even if an attacker mints many tokens, total connections to a single sandbox are capped. 20 allows legitimate multi-session workflows while bounding abuse.
Session reaper interval 1 hour No (compile-time constant) Garbage-collects expired and revoked sessions from the store. Keeps the session table clean without placing load on the DB.

Why two connection tiers matter: A per-token limit alone can be bypassed by creating many tokens. The per-sandbox limit is a second defense that caps total exposure regardless of how many tokens exist. Together they provide defense in depth.

Why TTL does not break UX: Every CLI command (connect, exec, forward, sync-up, sync-down, ProxyCommand) creates a fresh session token on every invocation via ssh_session_config() at crates/navigator-cli/src/ssh.rs:31. The token is consumed once to establish the CONNECT, then the tunnel lives independently via copy_bidirectional. A 24h TTL is orders of magnitude longer than any single-use window. Background port forwards are also unaffected — TTL only gates new connections, not existing tunnels.

Severity Assessment

  • Impact: High — a leaked token previously granted permanent SSH access with no time-bound limit
  • Exploitability: Medium — exploitation requires both a valid SSH session token AND valid mTLS credentials
  • Affected components: ssh_tunnel.rs (tunnel validation), grpc.rs (session creation/deletion), navigator.proto (SshSession schema), config.rs (TTL config)

Changes Made

  • proto/navigator.proto: Added expires_at_ms field (tag 7) to SshSession and (tag 8) to CreateSshSessionResponse. Backward-compatible additive change — existing sessions decode with expires_at_ms = 0 (no expiry).
  • crates/navigator-core/src/config.rs: Added ssh_session_ttl_secs config field (default 86400 = 24h). Set to 0 to disable expiry.
  • crates/navigator-server/src/grpc.rs: Compute and set expires_at_ms on session creation. Include in response. Clean up all SSH sessions when a sandbox is deleted.
  • crates/navigator-server/src/ssh_tunnel.rs: Check expires_at_ms during tunnel validation (0 = no expiry for backward compat). Enforce per-token (3) and per-sandbox (20) concurrent connection limits with in-memory counters. Added spawn_session_reaper background task (hourly) to delete expired/revoked sessions.
  • crates/navigator-server/src/lib.rs: Added ssh_connections_by_token and ssh_connections_by_sandbox counters to ServerState. Launch session reaper at startup.
  • architecture/gateway-security.md: Updated SSH Tunnel Authentication section with session lifecycle, connection limits, and updated residual risks table.

Tests Added

  • Unit: crates/navigator-server/src/ssh_tunnel.rs — 9 tests covering:
    • decrement_removes_entry_at_zero, decrement_reduces_count, decrement_missing_key_is_noop (connection counter logic)
    • per_token_connection_limit_enforced, per_sandbox_connection_limit_enforced (limit constants)
    • reaper_deletes_expired_sessions, reaper_deletes_revoked_sessions, reaper_preserves_zero_expiry_sessions (reaper with real SQLite store)
    • expired_session_is_detected, future_session_is_not_expired, zero_expiry_is_not_expired (expiry validation logic)

Documentation Updated

  • architecture/gateway-security.md: Added Session Lifecycle and Connection Limits sections under SSH Tunnel Authentication. Updated Residual Risks table to reflect that SSH tokens now have TTL and connection limits.

Verification

  • All 509 existing tests pass (cargo test --workspace)
  • E2E: 57/57 Python tests pass, 16/16 Rust e2e tests pass (1 pre-existing failure in custom_image unrelated to this change)
  • cargo fmt --check passes
  • cargo clippy --workspace --all-targets passes (no new warnings)

…ifecycle cleanup

Closes #22

SSH session tokens previously had no TTL and remained valid indefinitely.
This adds configurable token expiry (default 24h), per-token (10) and
per-sandbox (20) concurrent connection limits, session cleanup on sandbox
deletion, and a background reaper for expired/revoked sessions.
@johntmyers johntmyers added the topic:security Security issues label Mar 9, 2026
@johntmyers johntmyers self-assigned this Mar 9, 2026
@johntmyers johntmyers requested a review from drew March 9, 2026 18:26
@johntmyers johntmyers merged commit 55ec4d0 into main Mar 9, 2026
10 checks passed
@johntmyers johntmyers deleted the fix/security-22-ssh-session-expiry branch March 9, 2026 18:39
drew pushed a commit that referenced this pull request Mar 16, 2026
…ifecycle cleanup (#182)

* fix(security): add SSH session token expiry, connection limits, and lifecycle cleanup

Closes #22

SSH session tokens previously had no TTL and remained valid indefinitely.
This adds configurable token expiry (default 24h), per-token (10) and
per-sandbox (20) concurrent connection limits, session cleanup on sandbox
deletion, and a background reaper for expired/revoked sessions.

* fix(security): lower per-token concurrent connection limit from 10 to 3

---------

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic:security Security issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSH session tokens never expire

2 participants