docs(architecture): ADR-002 (russh pin) + ADR-003 (feature graph)

- ADR-002 (Accepted): pin russh 0.65 (0.65.0 published 2026-10-25
  after the POCs' 0.64.1 verification — six security fixes incl.
  three in internet-facing classes; breaks are handler-signature
  shapes only, wire evidence transfers); aws-lc-rs backend confirmed
  with a clean in-scratch build record (no provisioning; ring kept as
  the recorded fallback); flate2 off (no compression); rsa on (front-
  door compat — supersedes the research docs' earlier off-leaning;
  RC-tree posture accepted with a recorded stable-release trigger);
  des/dsa never; rust-version 1.89 lands with the dep
- russh.md: re-verification-at-architecture-time section added
  (0.65.0 delta: PublicKeyOffer/VerifiedPublicKey, async-trait
  removed, GHSA list, verified build + tree facts)
- ADR-003 (Accepted): five features (session/git/tunnel/
  dynamic-forward/sftp-placeholder), flat + independent, default [],
  two-direction symmetry rule (one name, both doors), client acceptor
  fail-closed (not russh's accept-and-drop), x11/auth-agent never
  features, honest advertisement per build
This commit is contained in:
glm-5.3-flash committed 2026-10-26 12:16:00 +00:00
1 parent 86bc2ccfd3
commit 9d2e962bec
4 files changed
+247 -5

No files matched your search

+2
View File
@@ -36,6 +36,8 @@ below are being written. No implementation until specs reach
| ADR | Title | Status |
|---|---|---|
| [ADR-001](decisions/001-alkssh-alpn-pin.md) | The `alk/ssh` ALPN pin — edge-termination contract for `SshAdapter` | Accepted |
| [ADR-002](decisions/002-russh-pin.md) | The russh pin — 0.65.0, aws-lc-rs, no compression, RSA in | Accepted |
| [ADR-003](decisions/003-feature-graph.md) | The capability feature graph — flat, independent, two-direction | Accepted |
| [ADR-004](decisions/004-auth-dispositions.md) | Auth dispositions — publickey-only, fail-closed, fingerprint identity | Accepted |
| [ADR-005](decisions/005-at-rest-key-custody.md) | At-rest key custody — vault-wrapped OpenSSH-PEM, decrypt-at-use | Accepted |
@@ -0,0 +1,99 @@
---
status: draft
last_updated: 2026-10-26
---
# ADR-002: The russh pin — 0.65.0, aws-lc-rs, no compression, RSA in
## Status
**Accepted** — 2026-10-26. A one-way door in practice (SSH negotiation
behavior crosses crate boundaries; bumps are deliberate, tested, and
recorded per the repo's wire-stability convention).
## Context
`russh.md` documents the crate inventory and the pin questions; the
2026-10-26 re-verification section carries the current facts. The POCs
validated against 0.64.1 (then-newest) with the `ring` backend; since
then **0.65.0** has been published (2026-10-25) with six security
fixes — three in classes an internet-facing SSH door cares about
(client EXT_INFO unbounded allocation, peer zero-max-packet-size
channel stall, pre-auth nested-certificate stack exhaustion) — and two
breaking changes confined to handler-signature shapes
(`PublicKeyOffer` / `VerifiedPublicKey`; `async-trait` feature
removed).
The family's crypto-posture context: `alktls`, `alkhttp`, and
`alknet` all standardize on `rustls` with the **`aws_lc_rs`** feature;
no family crate links `ring`. The provider choice matters only as
binary-composition hygiene — when alkforge's binary links alkssh
*and* the family's rustls stack, one shared primitive library is the
goal — and has nothing to do with TLS on the SSH wire.
## Decision
1. **Pin `russh = "0.65"`** (caret-compatible within 0.65.x, matching
the family's `"0.10"`-style convention; russh minors have broken
API before, so the caret stops at the minor). 0.65.0 over 0.64.1:
current published + the three security fixes above; the breaking
changes land inside the handler surface alkssh implements anyway.
POC evidence transfers (wire behavior unchanged); the
implementation phase re-verifies the POC-1 decline matrix against
0.65 as contract tests.
2. **Crypto backend: `aws-lc-rs`.** Family-confirmed (`alktls` /
`alkhttp` / `alknet` all `rustls(aws_lc_rs)`; nothing in-family
links `ring`) and it is russh's own default. Verified in-scratch:
builds clean in ~50 s with no external provisioning (prebuilt
bindings), tree `aws-lc-rs 1.18.2` / `aws-lc-sys 0.46.0`. The
POCs' `ring` build is recorded as the fallback if a target
platform ever fails to provision aws-lc — a deliberate,
recorded deviation, never drift.
3. **`flate2` off — no compression.** The POCs already ran
compression-free and vanilla OpenSSH was untroubled; SSH
compression is largely pointless at modern link layers and is the
CRIME-adjacent attack surface OpenSSH itself defaults off.
Bounded-resources and surface-reduction posture agree.
4. **`rsa` on.** This crate is the family's vanilla-SSH *front door*:
alkforge users bring their own keys, and RSA remains common in the
wild. Fingerprints work identically for RSA keys (the identity
model doesn't care), so enabling `rsa` buys real compatibility at
the cost of two RC-pinned deps (`rsa =0.10.0-rc.18`,
`pkcs1 =0.8.0-rc.4`). The RC-pinned tree (`ssh-key =0.7.0-rc.11`
exact-pinned by russh, plus these two) is **accepted for the
0.1.0 line** with the revisit trigger recorded: bump/relax when
`rsa` and `ssh-key` hit stable — a deliberate, recorded posture,
not drift. The earlier "rsa-off leaning" from the research docs is
superseded: it served the RC-avoidance goal and would have cost
front-door compatibility; front-door compatibility wins.
5. **`des` / `dsa` never.** russh's own "Danger: insecure" features —
no alkssh configuration may enable them.
6. **Manifest lands with the dependency**: `rust-version = "1.89"`
(russh's MSRV; supersedes the current 1.88) and, with it, the
edition-2024 bump the POCs compiled under.
## Consequences
- **Positive**: the pin is current, carries the security fixes, and
matches the family crypto posture; the tree is fully inventoried
(RC set identical to the POC tree); MSRV/licensing are explicit.
- **Negative**: the RC deps ship in a published crate's tree until
the revisit trigger fires; aws-lc-rs is the heavier build (proven
acceptable here, with `ring` as the recorded fallback).
- **Neutral**: ADR-004's dispositions are signature-agnostic — at
0.65.0 the key arrives as `VerifiedPublicKey.public_key` and the
new `algorithm` field is available for future hardening (server-side
reaction to client-signature-algorithm choice); no disposition
changes.
## References
- `docs/research/russh.md` (inventory + the 2026-10-26
re-verification section with the scratch-build record)
- `docs/research/poc-1-auth-bridge.md` (the 0.64.1 wire evidence;
backend note), `docs/research/poc-2-vault-keys.md`,
`docs/research/poc-3-mapping-bakeoff.md` (0.64.1 runtime facts)
- ADR-001 (the pin context), ADR-003 (feature graph the russh
features compose under), ADR-004 (auth dispositions on this surface)
- Family: `alktls`/`alkhttp`/`alknet` manifests (the aws-lc-rs
posture), family ADR-002 decision 5
@@ -0,0 +1,99 @@
---
status: draft
last_updated: 2026-10-26
---
# ADR-003: The capability feature graph — flat, independent, two-direction
## Status
**Accepted** — 2026-10-26. Builds the fail-closed capability contract
(family ADR-003 decision 3) into the crate's own feature names; the
decline paths are contract territory on every side of every feature.
## Context
`mapping-table.md` (POC-3-validated) split the SSH `session` channel
one channel type carries shell, exec, git, and SOCKS traffic, and
vanilla SSH multiplexes them all through session channels. The feature
graph is how alkforge gets an endpoint that is *structurally incapable*
of serving what it didn't enable: a git-only build accepts no shell, no
tunnels, no SOCKS — and, in the opposite direction, no capabilities a
client build didn't opt into.
Phase 0 validated the seams end to end (POC-3 stages A–D: git exec,
SOCKS-in-channel, direct-tcpip via the alktunnels bridge, session/pty,
all against published siblings) and the user's direction confirmed the
scope: full feature set except sftp (placeholder until alkfs lands).
## Decision
1. **The feature set** (cargo features on `alkssh`, all off by
default):
| Feature | Serves (inbound) | Consumes (outbound) | Family seam |
|---|---|---|---|
| `session` | session channels: pty/shell, pipe-mode exec | client pty/shell/exec/subsystem opens | alktty |
| `git` | git-parseable exec (dispatched out of session channels) | client git consumption of upstream servers | alkgit |
| `tunnel` | `direct-tcpip`, `forwarded-tcpip`, `direct-streamlocal`, the `tcpip_forward`/`streamlocal_forward` listen flow | client `direct-tcpip` opens; the client `-R` leg (listen request + forwarded-channel acceptor) | alktunnels |
| `dynamic-forward` | SOCKS5-in-channel on session channels (family-native client shape) | client-side SOCKS consumption | alksocks |
| `sftp` | *placeholder — declines until alkfs lands (OQ-13)* | client sftp half, also deferred | alkfs (future) |
2. **Flat and independent.** No feature implies another. `default = []`
— a default build has no capabilities at all; every unfeatured
channel type/open/request declines with a clean error. `git` does
not imply `session` (a git-only build serves the git-parseable exec
path by declining to parse to anything else — the parse-then-fail
contract from POC-3 stage A); `dynamic-forward` does not imply
`session` (SOCKS-in-channel rides session channels but is gated at
dispatch, before any serve).
3. **Two-direction symmetry.** Each feature name gates *both* the
server-direction (what an embedded alkssh serves vanilla clients)
and the client-direction (what a family app drives when dialing an
upstream SSH server, including the forwarded-channel acceptor rows).
One name, one capability, both doors — this is the symmetry rule,
now structural: a `git`-only build cannot dial a tunnel upstream
any more than it can serve one.
4. **Client-side fail-closed at the acceptor.** Server-initiated
channel opens (`forwarded-tcpip` is the real one — the `-R` data
path) are served only under a registered feature's acceptor
handler; unhandled/unfeatured accepted opens are **declined**, not
accept-and-dropped (russh's client default is accept-then-drop —
ours to fix; the reverse-runner extraction names it).
5. **Never features.** `x11` and `auth-agent` forwarding are not
feature-gated at all — always declined, at any configuration
(agent forwarding is the *anti-pattern* of the alkvault custody
model; x11 has no family seam). An ADR amendment is required to
ever feature-gate them.
6. **Honest advertisement per build.** What the wire advertises
mirrors what features are on: auth `methods` is always
`{publickey}` (ADR-004) server-side; client-side capabilities are
invoked only by the assembly, and unfeatured rows decline. There
is no "advertise and see" path.
## Consequences
- **Positive**: alkforge's ssh endpoint shape is a straight cargo
selection; the failure matrix (git-only, session-only, none) is a
tested decline matrix rather than a hope; the client and server
surfaces can't drift apart on capability naming.
- **Negative**: the split of `session` makes feature interplay a
real test matrix (per-feature decline paths on both directions are
contract tests — the AGENTS.md verification convention covers this
via `--all-features` runs plus the decline-matrix suite).
- **Neutral**: feature *names* are now contractual (downstream builds
specify them); renames would be semver-visible — hence the ADR, not
incidental naming.
## References
- `mapping-table.md` (the validated rows under these names + the
feature-split notes + the three POC-3 corrections)
- Family ADR-003 (decision 3 — the fail-closed capability features),
family ADR-002 (why exclusion matters for alkforge)
- `docs/research/reverse-runner.md` (client-direction surface the
symmetry rule serves; the acceptor default fact)
- `docs/research/poc-3-mapping-bakeoff.md` (stages A–D: the seams at
these names work end to end)
- OQ-13 (sftp placeholder tracking), OQ-02 (dispatch precedence
inside `session`)
+47 -5
View File
@@ -6,10 +6,30 @@ time before any pin is recorded.
## Version landscape
- Current published: **russh 0.64.1** (2026-10-05, docs.rs). 0.64.1 is
a security-fix-only patch of 0.64.0 (GHSA-4wc5-f2rc-q74m — unbounded
memory allocation in the agent protocol server from crafted
constraint records; no API change).
**Re-verified at architecture time (2026-10-26) — see the section at
the end of this file.** russh **0.65.0** was published 2026-10-25
(after the POCs verified 0.64.1 as newest); the pin ADR is written
against 0.65.0. The POC wire-behavior evidence transfers: the 0.65.0
breaking changes are handler-signature shapes, not wire behavior.
- Current published: **russh 0.65.0** (2026-10-25, GitHub releases).
Breaking: `Handler::auth_publickey_offered` now takes a
`server::PublicKeyOffer` and `Handler::auth_publickey` a
`server::VerifiedPublicKey` (read the key from the `.public_key`
field; a new `.algorithm` field exposes the client's signature
algorithm, allowing server-side reaction to it); the `async-trait`
dep/feature was removed. Security fixes in classes this crate cares
about: GHSA-4p9f-7p9m-5vfg (client: unbounded allocations via
EXT_INFO packets), GHSA-fw5c-8223-6h42 (peer-declared zero maximum
packet size permanently stalls channel writes),
GHSA-3hm4-p7h4-rvwp (pre-auth stack exhaustion via nested OpenSSH
certificates); the agent/pageant fixes are off our path (agent
forwarding declines, ADR-003).
- 0.64.1 (2026-10-05) was the POC pin — security-fix-only patch of
0.64.0 (GHSA-4wc5-f2rc-q74m — unbounded memory allocation in the
agent protocol server; no API change). The POC findings' evidence
and this file's earlier inventory facts were verified against
0.64.1 and carry to 0.65.0 as noted above.
- 0.64.0 (2026-10-04) broke API: dropped the
`legacy-ed25519-pkcs8-parser` feature (pre-0.43 non-compliant
ed25519 keys now hard-error); fixed GHSA-j7h2-66j4-ghjf (server
@@ -232,4 +252,26 @@ exists but is out of family scope (agent forwarding declines; see
- russh GitHub releases 0.64.0 / 0.64.1 (GHSA ids quoted above)
- alknet `main` `crates/alknet-core/src/auth/` — rough reference only,
explicitly not decision input (0.49-era API; verify nothing against
it)
it)
## Re-verification at architecture time (2026-10-26)
The "versions re-verified at architecture time" clause, executed. Facts
verified today via `cargo info` / GitHub release notes / a scratch
build (`/tmp/opencode/alkssh-pin-check`, disposable):
| Fact | Verified value |
|---|---|
| Current published | **0.65.0** (2026-10-25) — see Version landscape above |
| Backend feature | `aws-lc-rs` (dashed feature name; also russh's own default alongside `flate2`, `rsa`) |
| Scratch build | `default-features = false, features = ["aws-lc-rs", "rsa"]` — clean, ~50 s, **no external provisioning** (prebuilt aws-lc bindings cover this platform; `aws-lc-rs 1.18.2` / `aws-lc-sys 0.46.0`) |
| Tree (the pin set) | `ssh-key =0.7.0-rc.11` (exact), `rsa =0.10.0-rc.18`, `pkcs1 =0.8.0-rc.4`, `getrandom 0.4.3`, `rand_core 0.10.1` — same RC set as the POCs' 0.64.1 tree |
| MSRV | 1.89 (both 0.64.1 and 0.65.0) |
| License | Apache-2.0 only |
The earlier sections describe the 0.64.1-era surfaces (POC-verified);
`server::PublicKeyOffer`/`VerifiedPublicKey` and the removed
`async-trait` feature are the only inventory deltas. Wire behavior
(evidence base for ADR-001/004/005) is unchanged per the release
comparison; the re-verification of the decline matrix against the
actual pin is implementation-phase contract-test territory.