Skip to content

spike(transport): russh dual-role SSH proxy round-trip — operator → airship bastion → newt embedded sshd (cert auth both hops, no-PTY subsystem) #49

Description

@hartsock

Goal

Prove the load-bearing mechanics of the airship SSH hub design end-to-end with a throwaway PoC, before committing to the Transport trait / SshTransport refactor. The one round-trip to demonstrate:

operator (russh client) ──▶ BASTION (russh server + russh client) ──▶ PILOT (russh server, embedded sshd)
        cert auth hop 1                 terminate + re-originate               cert auth hop 2
                                                                              exposes a NO-PTY subsystem

If this round-trip works — cert-authenticated at both hops, terminating proxy in the middle, a constrained (non-shell) channel at the end, and per-operator attribution the hub cannot forge — every hard part of the hub design is de-risked.

Why (rationale)

The hub is the WAN/operator face of the mesh (long-haul, bastion traversal, Windows reachability — the gaps iroh/QUIC leaves open). Per the ownership decision in newt-agent docs/design/ssh-ca-trust-root.md §10, the Transport trait + SSH server live in agent-mesh, so the spike belongs here. The full hub design is in airship docs/design/ssh-hub.md. Reference bastion (same library, production): Warpgate (warp-tech/warpgate, russh-based) — quarry its SSH proxy path.

Current substrate (verified)

  • agent-mesh-transport exposes a concrete iroh Endpoint (bind/dial/accept, endpoint.rs) — no Transport trait yet. The spike does NOT need to build the trait; it proves russh mechanics standalone, informing the later refactor.
  • ssh-key 0.6 is already a workspace dep (Cargo.toml, used for GitHub cross-sign/verify) — the cert primitives (ssh_key::Certificate, CertType, principals, validity, signature verify) are already on hand.
  • Engine: russh 0.62 (Apache-2.0, tokio, client + server in one crate).

What the PoC must prove (the hard parts)

  1. Dual-role russh in one process — a server::Handler accepting a connection AND a client opening an onward connection, spliced (the bastion terminates and re-originates; this is the Warpgate model, NOT direct-tcpip passthrough).
  2. Cert-based mesh-membership auth at BOTH hops — validate an OpenSSH certificate in the server::Handler: signature chains to a test CA, valid_after/before current, principal matches, (stub) KRL check. russh does not wire certs end-to-end — this is our glue over ssh-key. Prove reject paths too: expired, wrong-CA, principal-mismatch, garbage.
  3. NO PTY / NO shell — the pilot server exposes only a named subsystem channel (stand in for newt-remote); pty_request/shell_request are rejected/unregistered. Confirm a shell request fails.
  4. ring crypto backend — build with russh's ring backend (not aws-lc-rs), to keep the "no C" posture and portability.
  5. Per-operator attribution — end-to-end and hub-unforgeable (NON-NEGOTIABLE). Attribution must NOT collapse to the hub's identity. The operator signs the subsystem payload (an ed25519 envelope); the pilot verifies it against the operator's key; and a payload injected or altered by the terminating bastion is REJECTED at the pilot. The bastion authenticates the transport hop (its own attenuated cert) but cannot forge who acted. This is the operator directive (airship docs/design/ssh-hub.md §13) — prove it, don't stub it.

Watch-items to resolve (record findings in the issue)

  • MSRV. agent-mesh floor is 1.75; the russh spike in newt-agent passed on 1.88. Determine russh 0.62's actual MSRV and whether it forces bumping agent-mesh-transport's rust-version (or isolating the SSH code in a crate with a higher floor). This is the most likely blocker — check first.
  • Windows. SSH exists in this architecture partly because iroh/QUIC is flaky on Windows. Confirm the PoC (esp. the ring backend + any streamlocal/subsystem paths) builds and runs on Windows, or note what doesn't.
  • API churn. russh is 0.x (0.61→0.62 already broke APIs). Pin the exact version used.

Acceptance criteria

  • A single test binary runs all three roles (operator client, bastion, pilot server) in-process (or as three tasks) and completes a round-trip: operator sends bytes over the subsystem channel → bastion proxies → pilot echoes → operator receives.
  • Both hops reject a connection presenting an invalid cert (at least: expired + wrong-CA + principal-mismatch), and accept a valid one.
  • A shell/pty request is rejected by the pilot server.
  • Per-operator attribution proven: the pilot attributes a message to the operator via the operator-signed envelope; a message forged or altered by the bastion is rejected (the hub cannot impersonate the operator). This must pass even though the bastion terminates SSH and sees plaintext.
  • Builds on the ring backend.
  • Findings recorded for MSRV (does it force a floor bump?) and Windows (does it build/run?).
  • A short writeup (spike verdict: GO / GO-with-caveats / NO-GO) appended here or as a doc, feeding phase 2 (the Transport trait + SshTransport).

Out of scope (this is a spike)

  • The real Transport trait / IrohTransport refactor (phase 2 — informed by this).
  • Wiring to the real agent-mesh CA, KRL topic, or roster/discovery (use test fixtures).
  • The real newt-remote subsystem protocol (a stub echo subsystem stands in).
  • Session recording, routing-by-username, the airship deployment (later phases).
  • Production hardening — spike code is throwaway.

Refs

  • Design: airship docs/design/ssh-hub.md (the hub this de-risks)
  • Substrate: newt-agent docs/design/ssh-ca-trust-root.md (CA, member certs, SshCaveats, russh spike GO)
  • Reference bastion: warp-tech/warpgate (russh-based terminating SSH proxy)
  • Related existing spike issue: spike(transport): does the handshake expose a transcript hash for SAS binding? #30 (spike(transport): handshake transcript hash for SAS binding)
  • Payload (later): Gilamonster-Foundation/newt-remote (the interactive attach surface the channel will carry)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions