DreamLake

SSH through nymph

Status: Partly implemented. Control-plane sessions and relay are merged; Nymph integration and runtime deployment remain open. Tracking: Master #247, implementation PR #22, merged design #23.

2026-09-15 — Control-plane sessions and relay

Control-plane PR #53 merged as 111f7b11, adding session creation, reading and closure under #51. Each session stores its credential, grant, worker, daemon boot and host-key snapshot. Retries retain the session ID and original deadlines; a different body conflicts, and a closed session cannot reopen. Host-key aliases include namespace and worker identity.

Transactions enforce two live sessions per worker and 32 per namespace. Grant or token revocation closes matching sessions and clears client public keys before replying. Session requests require an explicit credential even in development open mode; sibling namespace membership does not authorize access.

The control plane now accepts signed SSH capability advertisements. It binds the verified signer to the exact worker and namespace, rejects unapproved host-key or Unix-account replacement, and closes sessions on withdrawal or boot change. SSH requests use durable Mongo nonce records; replay-store failure denies the request rather than falling back to the general daemon nonce cache.

A background sweep closes expired sessions and removes their client keys without a client read. It also removes nonce records after their signature-validity window. Shutdown stops the timer and waits for its current sweep.

Client and daemon attach tickets are short-lived, stored as hashes, and bound to one session and side. Reissue invalidates the old ticket; concurrent consumption admits one connection. Relay-state primitives bind both sides to the same owner; consumption alone does not mark the backend ready or the session active.

Signed daemon leases revalidate the credential, grant, worker boot and signer. Each lease lasts at most 15 seconds and cannot extend the original connection or session deadline. Expiry closes the session and removes client keys and tickets.

The control plane includes a WebSocket relay. It authenticates header-only tickets before upgrading, pairs both sides on one relay process, forwards binary DATA/FIN frames and applies backpressure. Directional queues are capped at 256 KiB and 128 frames; compression is disabled. A durable owner epoch fences out replaced processes and closes their attached sessions.

Local connection, session and lease timers close sockets even if database reads stall. Authorization checks run every second. Only a newer persisted lease extends the local timer; expiry, revocation and transport failure close both owned sockets.

Validation at 48415e2 passed 41 real-Mongo HTTP/WSS tests and 720 unit tests with 14 skipped. WSS tests use an ephemeral certificate trusted explicitly by the clients. They cover a 2 MiB binary round trip with directional FIN, a 32 MiB paused-receiver transfer, protocol and ticket rejection, data after FIN, expired leases and replacement of the relay process. Temporary certificates, sockets and the isolated database were cleaned up. A fault-injection check confirmed that both sockets close at the 15-second lease boundary while an authorization transaction remains stalled. Complete-server startup with the relay enabled passed. Linux CI passed for the same source before merge.

The relay is off by default. Enabling it requires SSH_RELAY_ENABLED=true on one active relay process, the updated schema, and a trusted HTTPS ingress that routes both legs to that process. Multi-replica routing is not implemented.

Nymph capability publication, backend dispatch and lease handling, native CLI integration, and end-to-end SSH behind NAT remain open. The WSS tests use test peers; they do not establish Nymph/OpenSSH acceptance. No remote Nymph SSH capability or control-plane deployment has been enabled.

2026-09-15 — Control-plane worker grants

Control-plane #51 tracks SSH authorization and session integration. The first implementation adds admin-only grant creation, listing and revocation for an exact worker and namespace. Development open mode, ordinary namespace credentials and sibling namespaces in the same organization cannot administer SSH grants. Client private keys remain local.

Mongo transactions serialize duplicate creation and the limit of 100 active grants per worker. Revocation retains its original timestamp; a later grant gets a new ID. Grant creation rechecks credential revocation on replay. ObjectId case is normalized before constructing the unique binding.

A legacy-record check found that incrementing an absent revision field did not serialize capacity admission. The correction writes a fresh transaction fence to the worker and token documents. The test removes those fields first, then verifies that concurrent requests still respect the limit. The failed run cleaned its isolated database; all 10 real-Mongo checks passed with the correction, including capacity admission against legacy records.

Real-Mongo HTTP checks cover authorization denials, strict input, namespace binding, concurrent duplicates and capacity, restart persistence, revoke/regrant and a concurrent token-revocation race. The existing suite passed 717 tests with 14 skipped. All 10 real-Mongo checks passed, including the final ObjectId regression. Implementation PR #52 merged as 69b15e83. Linux CI passed the same 717 existing tests (14 skipped) and 10 real-Mongo tests (none skipped). The complete server also started against an isolated database: unauthenticated grant reads returned 401 and an authenticated unknown-worker request returned 404. The owned server, Mongo process and temporary database were cleaned up.

Grant management is merged but not deployed. Sessions, tickets, leases, dispatch and relay integration remain open; creating a grant does not yet provide SSH access. Run the isolated transaction checks from the control-plane checkout with python3 scripts/test-ssh-grants.py after installing dependencies and generating the Prisma client. The script requires local mongod and mongosh, creates its own replica set and database, and removes them when it exits. CI runs the same HTTP checks against an isolated Mongo container.

2026-09-15 — Current Nymph integration

Draft Nymph #22 at 21ebe65 now incorporates main 9b3af89, preserving ownership, cancellation and private claim admission. The module-export merge conflict retained both implementations. All-features testing passed 422 tests with 12 ignored. After a formatting-only module-order correction, 283 focused library/SSH tests passed with two ignored; all ten SSH cases passed. Formatting checks passed.

Validation receipt. Both current-head Linux workflows passed: all-features Test passed 428 tests with 12 ignored; Check passed formatting, Clippy and its test suite. SSH remains unwired and disabled; CP grants, sessions, tickets and leases, daemon backend/capacity, relay and CLI integration remain open. The backend proof below does not establish that integration.

2026-09-15 — Same-user OpenSSH backend proof

An isolated sshd -i on bos14 passed native SSH checks under UID 107400011: exact UID, remote exit status 7, a 2 MiB binary half-close round trip, PTY, SFTP upload/download/removal, wrong client-key rejection, host-key mismatch rejection and administrative forwarding denial.

Strict ownership checking stayed enabled. The initial fixture under /tmp failed because OpenSSH rejected its parent directory modes. The successful fixture used the existing mode-700 runtime directory owned by the Unix user. Each connection used a socket pair; no listening SSH service was installed. A separate readback confirmed all four temporary remote stages removed.

This used existing SSH access to carry the test stream. It does not prove the proposed outbound relay, CP authorization, revocation, stable Nymph host keys, or daemon/CLI integration. Nymph PR #22 remains draft. Backend receipt.

2026-09-12 — first implementation

The intended experience is ordinary ssh to a machine behind NAT: nymph opens an outbound relay connection, and a local proxy connects the native SSH client. No Python SDK, public host IP or inbound SSH port is required by the proposed design.

At nymph commit ce1deac, src/ssh.rs implements local admission checks and a binary frame codec. It rejects local opt-out, root/missing-backend capability, wrong daemon instance, invalid identifiers, expired/overlong deadlines and malformed Ed25519 public keys. Unknown fields cannot select another account or destination. DATA/FIN frames preserve binary bytes and track EOF independently per direction.

The public HTTP design uses RFC 3339 deadlines. The internal daemon request uses integer connect_by_unix_ms and expires_at_unix_ms, plus session ID, daemon instance ID and one client public key. Private keys stay at their endpoints.

Validation

222 local tests passed: 188 library tests, 24 existing codec tests and 10 SSH tests. The SSH cases include JSON/msgpack round trips, request denials, deadline bounds, malformed frames and a 2 MiB binary round trip. This is codec evidence, not a real SSH connection or remote acceptance result.

bash
CC=/usr/bin/clang CXX=/usr/bin/clang++ cargo test --locked --lib --test codec --test ssh

The compiler overrides avoided an incompatible ambient GCC on the test Mac.

Remaining work

  • Dispatch and deduplicate sessions; enforce capacity.
  • Start the same-UID SSH backend and connect the WSS relay with bounded queues.
  • Implement CP grants, tickets, leases and revocation, then native CLI setup/proxy.
  • Pass real local and remote SSH acceptance, including UID, file transfer, denial, bounded revocation and fresh-session recovery.
  • Define forwarding policy before testing port forwarding and VS Code support.

Local and remote acceptance guides contain examples for the proposed feature. These are not current release commands. The merged design does not enable SSH on any host. See Unreleased.