# alkssh SSH adapter for the alk family: a thin wrapper around [russh](https://docs.rs/russh) terminating the vanilla-ssh wire and bridging to/from alkcall channels — the SSH counterpart of the alkhttp adapter (`family` ADR-003). Vanilla clients terminate vanilla ssh; family protocol halves (alktty, alktunnels, alkgit) receive their own chunk planes; the door itself speaks no family protocol and owns no deployment policy. The crate is a **door, not a backend**: no binary, no front doors — a downstream app (alkforge) embeds it, enables the features it wants, and brings identity/ACL configuration and the family fabric handles. It is two-directional: a server mount serves vanilla ssh into the family, and a dial points family ops at a vanilla `sshd`. ## The feature graph — fail-closed `default = []` by design: **a default build has no capabilities and can serve nothing but clean declines.** Every channel-type mapping is a cargo feature; each feature is flat and independent (no feature implies another): | Feature | Maps | Family protocol | |---|---|---| | `session` | pty / shell / exec (pipe) rows → chunk-framed session | alktty | | `git` | the exec-only git rows (`git-{upload,receive}-pack`) | alkgit | | `tunnel` | `direct-tcpip` / `direct-streamlocal`, the `-R` legs | alktunnels | | `sftp` | reserved decline placeholder (OQ-13) — always declines until the alkfs mapping lands | — | Non-mapped surface (x11, `auth-agent`, the `env` request, git *subsystem* requests, `sftp` until alkfs) declines fail-closed in every build: never a half-capability, never a silent fallback, and never advertised — an unfeatured channel type is declined, not advertised. A request whose config fields are unset declines too (an unset seam or shell argv configures nothing — the feature alone is not a capability). ## Usage The assembly injects the seams: an alkcall `IdentityProvider` (verified fingerprint → identity), key sources, and the per-feature family handles. The examples below are the crate's compile-checked doctests (see the crate docs in `src/lib.rs`). A server mount — per accepted TCP connection, run the door: ```rust,no_run use std::sync::Arc; use async_trait::async_trait; struct Identity; impl alkcall::core::auth::IdentityProvider for Identity { fn resolve_from_fingerprint(&self, _: &str) -> Option { None // the assembly's registry decides } fn resolve_from_token(&self, _: &alkcall::core::auth::AuthToken) -> Option { None } } struct HostKeys; #[async_trait] impl alkssh::HostKeySource for HostKeys { async fn load(&self) -> Result, alkssh::KeySourceError> { Err(alkssh::KeySourceError::Absent) // sketch; vault-backed in prod } } let serve = alkssh::ServeConfig::new(Arc::new(Identity), Arc::new(HostKeys)); let adapter = alkssh::SshAdapter::new(serve).await?; // per accepted TCP connection from the assembly's listener: // adapter.handle(stream).await?; ``` A dial — family ops pointed upstream at a vanilla server: ```rust,no_run use std::sync::Arc; use async_trait::async_trait; struct DialKey; #[async_trait] impl alkssh::KeySource for DialKey { async fn load(&self) -> Result { Err(alkssh::KeySourceError::Absent) // sketch; vault-backed in prod } } let conn = alkssh::dial(alkssh::DialConfig::new( "hub.example:22", "runner", Arc::new(DialKey), Arc::new(alkssh::RejectAll), )) .await?; // op rows (under their features): conn.exec, conn.tunnel_direct, // conn.git_service, conn.reverse_listen — see the crate docs. conn.close().await; // clean disconnect — drop is never the clean path ``` Minimal builds compile and doctest with no features enabled — ```console $ cargo build $ cargo test ``` — and every capability row additionally rides its feature build (the matrix: no features / `git` / `session` / `tunnel` / `sftp` / `--all-features`). ## Key custody and security posture - **Keys are sources, never static material** (alknet-main's `main` key model, ADR-005): the dial/host key seams are decrypt-at-use handles over an injected [`alkvault`](https://docs.rs/alkvault) service — keys held encrypted at rest, decrypted at use, zeroized through the seam. The vault is a secret store on this path, not a key generator. - **No env vars, no config-file key material, no secrets on the wire.** The standing family posture (family ADR-002 decision 5). - **Publickey-only authentication**, exactly one attempt — no password, keyboard-interactive, cert, or fallback methods exist on the surface. - **Host-key trust is assembly policy**: the crate ships a `HostKeyPolicy` seam with a fail-closed default (`RejectAll` — a dial with it connects to nothing) and **no accept-all built-in**; any permissive policy is the assembly's to write. - **Injected identity**: every wire-exposed op is checked against alkcall's `AccessControl::check(peer_identity)`; internal ops are never wire-callable; there is no unauthenticated capability. - **Bounded resources everywhere** (`Limits` — wall clock always set, channel caps, listener caps, window/packet budgets, auth attempt limit): the crate never constructs an unbounded session. Known, recorded limits ride the published docs (the crate-root rustdoc §Known limits). - **Every disabled feature declines**: the decline matrix is contract territory tested in every build. ## Documentation - Crate docs: `cargo doc --open` (the crate-root docs carry the feature graph and the known-limits records). - `docs/architecture/` — the authoritative contract: the spec (`overview.md`, `mapping.md`, `client.md`, `auth.md`), ADRs 001–006, and the open-questions tracker. ## License MIT OR Apache-2.0 — see `LICENSE-MIT` / `LICENSE-APACHE`.