Skip to main content

Terminal Security

Session Token Authentication

Every terminal session requires a cryptographically signed token. The backend generates a short-lived JWT-like token before sending the terminal_start command to the agent. The agent verifies the token before allocating a PTY.

Token Format

Compact base64url(payload).base64url(signature) using ed25519 (NATS account keypair).

Token Claims

ClaimDescription
session_idUUID tying the token to one specific session
agent_idAgent verifies this matches its own registered agent ID (not host_id)
loginLinux user the PTY should run as
user_idWho initiated the session (audit trail)
expExpiration — 1 minute from issuance
agent_id vs host_id

The token uses agent_id (the UUID assigned to the enrolled agent) rather than host_id. This means a replacement agent on the same host with a different agent_id will reject tokens issued for the old agent — ensuring session tokens are tightly bound to the specific agent process.

Verification Flow (Agent-Side)

  1. Verify ed25519 signature against the NATS account public key
  2. Check token is not expired (exp > now)
  3. Check agent_id matches the agent's own registered ID
  4. Check login matches the requested Linux user
  5. If any check fails → reject, log warning, send error ack — no PTY allocated

Key Properties

PropertyHow It's Enforced
No unauthorized shellsAgent requires valid token signature from trusted account key
No cross-host replayToken embeds agent_id, agent rejects mismatches
No login substitutionToken embeds login, agent rejects mismatches
No replay after expiry1-minute TTL, checked before PTY allocation
No token forgeryed25519 private key (account seed) never leaves the backend

Reconnect Security

When the frontend auto-reconnects after an unexpected disconnect, the backend issues a new session token for the reconnect attempt. The reconnect is only permitted if all of the following hold:

  1. The reconnecting user is the same user who started the original session (user_id matches)
  2. The target host matches the original session's host_id
  3. The original session has not ended — the PTY must still be alive within the disconnect_timeout linger window

If any check fails, the reconnect is rejected and a new session must be initiated from scratch. This prevents session hijacking by ensuring that even during the linger window, only the authenticated session owner can resume the shell.

First-Message WebSocket Auth

The session token is transmitted as the first WebSocket message after the connection is established — it is never included in the URL as a query parameter.

Why Not a Query Parameter?

Embedding tokens in URLs causes them to appear in:

  • Web server access logs (logged by default)
  • Browser history and bookmarks
  • Referrer headers sent to third-party resources
  • Proxies and load-balancer logs

By sending the token as the first binary WebSocket message, the token travels only over the encrypted WebSocket payload and is never exposed in any URL.

Auth Flow

Browser → Backend: WebSocket upgrade (no token in URL)
Backend: Accept connection, wait for first message
Browser → Backend: First message = base64(session_token)
Backend: Validate token, look up session state
Backend → Browser: Auth OK → begin I/O relay
Auth fail → close WebSocket with 4001 code

The WebSocket connection is terminated immediately if the first message is not a valid session token, or if it arrives after the 10-second auth timeout.

Transport Security

Encryption in Transit

All traffic is encrypted via TLS:

HopEncryption
Browser → BackendWSS (WebSocket over TLS) via Cloudflare
Backend → NATSTLS with strong cipher suites
NATS → AgentTLS with strong cipher suites

NATS Subject Isolation

Terminal I/O uses scoped NATS subjects that prevent cross-tenant data leakage:

proxima.{clientSlug}.{envSlug}.{hostID}.terminal.out.{sessionID}
proxima.{clientSlug}.{envSlug}.{hostID}.terminal.ctrl.{sessionID}

Agent JWT permissions restrict each agent to publish/subscribe only under its own host prefix. The NATS server enforces this at the protocol level.

What the Backend Can See

The backend relays PTY traffic but does not store it. An operator with access to the backend process could theoretically observe terminal I/O in memory. This is equivalent to Teleport's proxy model, where the proxy terminates SSH and re-encrypts to the node.

RBAC

See the RBAC & Permissions page for:

  • Role-based terminal access configuration
  • Allow/deny rules with label matching
  • Multi-role merge semantics
  • Validation constraints

Multi-Party Session Access

  • Observers require terminal_sessions:read permission
  • Participants require terminal_sessions:write permission
  • Client scoping enforced — peers must have access to the session's client
  • Peer join/leave events are recorded in the audit trail
  • Session owner always sees who is connected — no silent observation
  • Maximum 10 peers per session prevents resource exhaustion

Rolling Upgrades

The session token system supports rolling upgrades:

  • Backend deployed first (no agent update): Backend sends signed tokens, but old agent ignores them (no AccountPubKey configured). Terminal works without verification.
  • Agent deployed first (no backend update): Agent has AccountPubKey but backend sends empty tokens. Agent skips verification when AccountPubKey is empty.
  • Both deployed: Full verification active.

After both are deployed, set AccountPubKey on the agent (extracted automatically from the user JWT issuer) and verification is enforced.

Login Template Sandboxing

Login templates in terminal specs use Go's text/template with restricted execution:

  • Restricted function set: Only lower, upper, replace, trimPrefix, trimSuffix, default, and printf are available. No call, no OS access, no file I/O.
  • Execution timeout: Templates must complete within 1 second.
  • Save-time validation: Templates are parsed and dry-run with dummy data when the role is saved. Invalid templates are rejected before they reach production.
  • Output validation: Resolved logins must match the Linux username regex. Invalid results are filtered with a warning log.

File Transfer Security

  • User isolation: File operations run as the session's Linux user — OS file permissions are enforced
  • SHA-256 integrity: Every transfer is hashed and logged in the audit trail
  • Path validation: Path traversal (..) is rejected
  • Size limit: 100MB per file, enforced on browser, backend, and agent
  • Audit trail: Every upload and download is recorded with filename, path, size, and SHA-256 hash
  • Dedicated NATS subject: File data flows on a separate subject from terminal I/O, preventing interference

K8s Node Terminal Security

Terminal access to Kubernetes nodes uses nsenter to enter the host namespace from the DaemonSet agent pod.

DaemonSet security context:

  • hostPID: true — shares the host's PID namespace (required for nsenter -t 1)
  • SYS_PTRACE capability — allows ptrace of PID 1 to enter namespaces
  • SYS_ADMIN capability — allows mount namespace operations
  • readOnlyRootFilesystem: true — pod filesystem remains read-only
  • Not privileged: true — only the minimum capabilities needed

Why not privileged: true? Privileged mode grants unrestricted host access and is a security audit red flag. Targeted capabilities are sufficient for nsenter.

User isolation: Sessions run as the target Linux user via su -, not as root. Host user creation and sudoers rules execute inside the host namespace through nsenter.

Host User Security

When auto-creating host users:

  • Password expirychage -E 1 expires the password immediately. Users can only connect via Proxima terminal, never via local password login.
  • Tracking groupsproxima-keep and proxima-drop system groups identify managed users.
  • Root required — the agent must run as root to create users (useradd, chage, etc.).
  • Home directory — created manually with 0700 permissions, /etc/skel files copied (symlinks skipped).
  • Cleanup — drop-mode users are deleted by a background loop every 5 minutes, with a 30-second grace period and linger-awareness.

Sudoers File Management

When host_sudoers is configured:

  • Files are written to /etc/sudoers.d/proxima-<username> with mode 0440
  • Validated with visudo -cf before activation — invalid files are deleted immediately
  • In drop mode, sudoers files are cleaned up with the user
  • Rules are prefixed with the login username

Session Recording Security

  • Encryption at rest: Recordings stored in Cloudflare R2 with server-side encryption (AES256)
  • Presigned URLs: Download links expire after 1 hour — no permanent direct access to storage
  • Per-client retention: Each client configures retention (default 90 days). Expired recordings are automatically deleted from R2
  • Access control: Users can view their own session recordings. Admins with terminal_sessions:read can view all recordings
  • Agent-side recording: Recordings are written on the agent during the session, transferred to backend via NATS, then uploaded to R2. Agent never has storage credentials
  • Size cap: 10MB gzipped per recording prevents runaway storage from heavy output sessions

UID Range Security

  • System UIDs (below 1000) are excluded — UID ranges must start at 1000+
  • Minimum range size of 100 prevents trivial hash collisions
  • UID allocation is deterministic (FNV-1a hash) — no central state to corrupt
  • On collision, falls back to natural allocation with a warning log