Files
IronStorage/docs/ssh-transport-audit.md
Chili Palmer a3da9fda69
Some checks failed
Dependency security audit / rustsec (push) Has been cancelled
Complete SSH transport release audit (#118)
2026-08-25 22:07:47 +02:00

7.6 KiB

Pure-Rust Git-over-SSH release audit

Reviewed 2026-08-25. This is the final milestone-07 matrix for the optional storage ssh feature. The production transport is a Russh session connected directly to the configured server; it opens only Git upload-pack or receive-pack channels and never delegates to a local Git, SSH, shell, transport helper, or proxy process.

Remote lifecycle matrix

The two spellings enter one RemoteEndpoint::Ssh contract. URI paths remain absolute; scp-like paths remain account-relative. The tests below run a pure-Rust in-process SSH server with synthetic keys and Git objects.

Surface ssh:// scp-like Executable evidence
Parse, add, set, get, reject helpers yes yes ssh_remote_endpoints_parse_to_one_typed_contract, ssh_feature_allows_both_remote_forms_through_add_set_and_get, configuration rejection tests
Branch discovery and clone yes yes both_ssh_url_forms_drive_discovery_clone_fetch_fast_forward_merge_and_conflict
Explicit fetch and fast-forward pull yes yes same full-lifecycle test, including chunked advertisements and packs
Three-way merge and typed conflict pull yes yes same full-lifecycle test; conflicts preserve the local worktree
New, fast-forward, and already-current push yes yes new_branch_fast_forward_and_already_current_push_are_confirmed
Pull-then-push synchronization yes yes synchronization_pulls_then_pushes_over_both_ssh_url_forms
Progress, cancellation, and retry transport-neutral transport-neutral controlled Git tests, slow upload/push cancellation, unknown-host confirm/retry, encrypted-key reject/retry, and frontend interaction tests
CLI, TUI, and desktop presentation typed SSH typed SSH frontend selection, masked prompt, cancellation, authentication-error, conflict, progress, and HTTPS regression tests

The in-process server implements the same git-upload-pack '<path>' and git-receive-pack '<path>' exec-channel boundary used by a Gitea SSH endpoint. It validates both service commands, packet-line advertisements, chunked pack streams, receive status, and non-fast-forward behavior without a Git or OpenSSH installation. A real Gitea smoke test is optional and must use a disposable repository and synthetic key; it adds no protocol coverage and must never use or print a developer key.

Authentication and failure matrix

Risk or compatibility case Enforced result and evidence
Key files Ed25519, ECDSA P-256/P-384/P-521, and RSA/SHA-2 host and user keys authenticate; encrypted OpenSSH keys require the matching protected passphrase. Identity and known-host files are bounded and symlinks are rejected.
Existing agent The agent is opt-in, never launched, and one configured SHA-256 fingerprint is selected from at most 64 identities. Missing socket, missing identity, and rejection are distinct typed failures.
Host identity Exact, hashed, default-port, and bracketed non-default-port entries are supported. Unknown keys stop before authentication and require explicit persistence; changed or revoked keys stop before authentication and cannot be replaced through confirmation. Confirmation is bound to the configured host and port.
Algorithms The client owns an explicit allowlist: ML-KEM/X25519, Curve25519, SHA-2/SHA-512 finite-field KEX; Ed25519, ECDSA, and RSA/SHA-2 host/auth signatures; ChaCha20-Poly1305 or AES-GCM/CTR; SHA-2 MACs; no compression. DSA, RSA/SHA-1, SHA-1 KEX/MAC, CBC, none, and host certificates are excluded. A unit test rejects policy regression.
Paths and commands Absolute, relative, and ~user paths are preserved as one shell-quoted argument for exactly one built-in Git service. Quotes, whitespace, metacharacters, environment-looking text, and leading options cannot select another command.
Network and server failures Reserved-name DNS failure and refused connections are typed as network unavailable. Channel rejection, non-zero exit, signal, malformed packet/pack/status, early EOF, and disconnect are separately exercised. Remote diagnostics are control-sanitized and capped at 8 KiB.
Slow/chunked I/O and cancellation Channel chunks are capped at 32 KiB with queues of eight chunks. Connection and service completion time out after 30 seconds. Slow upload cancels without a destination; push cancellation after transmission returns unknown outcome and never advances tracking or blindly replays.
Rollback and ambiguity Clone uses a private sibling and atomically installs only a complete repository. Fetch/pull failures preserve refs/worktree. Push accepts success only after unpack, ref status, zero exit, and clean close; ambiguous completion requires a fresh fetch. Sync never pushes after a failed or conflicted pull.
Secrets Key/passphrase bytes use zeroizing storage types, never configuration, history, arguments, diagnostics, notifications, or clipboard. A prompted passphrase is fingerprint-bound and persisted only after successful authentication and Git work.

Dependencies, advisories, and size

Russh is exactly pinned to 0.63.1 (Apache-2.0), requires Rust 1.85, and is built without default features using only Ring and RSA support. Tokio is pinned by the lockfile at 1.53.1 (MIT) with only I/O, macros, network, multi-threaded runtime, synchronization, and time features. The selected licenses are compatible with IronStorage's MIT release; DEPENDENCIES.md records the full decision.

cargo audit 0.22.2 reports RUSTSEC-2023-0071 for rsa 0.10.0-rc.18 through Russh and for rsa 0.9.10 through the pre-existing OpenPGP/packaging graph. RustSec lists no patched release. The finding is not suppressed from the manual audit: it remains documented here and in DEPENDENCIES.md. The Gitea security workflow ignores only that named no-fix advisory so every other new vulnerability still fails pushes, pull requests, and the weekly audit.

On the reviewed release build, storage full without SSH contains 422 unique packages and a 15,961,360-byte optimized rlib. full,ssh contains 489 packages and an 18,376,592-byte rlib: 67 packages and 2,415,232 bytes (15.1%) added at the library artifact. Final executable size varies by target and linker, so release package sizes are recorded with the issue verification rather than treated as a stable source invariant.

Target and packaging contract

The CLI, TUI, and desktop manifests explicitly enable ssh; the tagged release workflow builds those ordinary artifacts for ARM64/x86-64 Linux and macOS and x86-64 Windows. Target-specific cargo tree -e features checks must show Russh through storage ssh on all three platforms. The macOS application package must contain all three SSH-enabled binaries.

The Apple bridge explicitly selects storage full with default features off and does not select ssh. Both iPhone simulator and device static libraries therefore exclude Russh while retaining typed SSH endpoint parsing and the pre-connection unsupported-transport error. Apple archives must preserve that graph until an Apple target explicitly opts in and adds a platform-specific host/key interaction design.

Deliberate OpenSSH differences

IronStorage does not read arbitrary OpenSSH configuration, ProxyCommand, Match, URL rewrites, separate push URLs, host certificates, password or keyboard-interactive authentication, agent forwarding, arbitrary remote commands, local transport helpers, or local-path remotes. These are explicit security boundaries, not partial implementations. HTTPS smart Git remains the unchanged alternative transport and shares the same embedded repository, merge, rollback, progress, and cancellation contracts.