docs(architecture): review-pass cleanup — inconsistencies, stale refs, scaffolding refresh

- OQ-02: rewritten as a clean resolved statement; contradictory
  deferred-table row removed
- OQ-05: 'old-code anti-pattern' framing corrected — ephemeral-fleet
  accept-and-continue is a legitimate case-2 HostKeyPolicy; the crate
  rule is 'no hard-coded trust model', not 'accept-all forbidden';
  dangling 'expected ADR-007' pointer replaced with the real contract
  locations (auth.md, client.md)
- README: OQ counts corrected (6 resolved / 3 open / 4 deferred);
  sibling references normalized to real ADR IDs (alkcall 010 is alkhttp
  014's analog; 'ADR-016-analog' was a wrong number)
- client/auth: OQ-07 status corrected to resolved-at-adapter-level;
  TOFO typo; TOFU framing
- ADR-004: dangling 'ADR-008' -> serve-config defaults
- ADR-006: SOCKS5 typo
- mapping: alktty typo; '(OQ-… note below)' placeholder -> the real
  POC-3 note reference
- research: cross-repo numbers qualified (alktty OQ-44, alkvault
  ADR-021); reverse-runner lesson 5 reframed per correction
- scaffolding: Cargo.toml description/comments and src/lib.rs doc
  comment updated against ADR-002/003/006 (dynamic-forward retired,
  git exec-only, russh pin decided); CHANGELOG logs POC completion and
  ADRs 001-006

Verification: cargo test, cargo clippy --all-targets -- -D warnings,
cargo fmt --check, cargo check — all pass
This commit is contained in:
glm-5.3-flash committed 2026-10-27 10:00:11 +00:00
1 parent fe9f97b71d
commit 263f91fdb7
15 files changed
+124 -86

No files matched your search

+16 -1
View File
@@ -18,4 +18,19 @@ this crate adheres to [Semantic Versioning](https://semver.org/).
`auth-bridge.md` (fingerprint → identity server path; vault-backed
at-rest client key storage — the vault as secret store, not key
derivation), `mapping-table.md` (channel type/request → family seam
+ feature), `pocs.md` (POC-1..3 plan — documented, not yet run).
+ feature), `pocs.md` (POC-1..3 plan).
- Phase 0 completed — all three POCs passed against the current
published russh with vanilla tooling (121 checks total); findings
in `poc-1-auth-bridge.md` (31), `poc-2-vault-keys.md` (39),
`poc-3-mapping-bakeoff.md` (51, three mapping-table corrections
recorded), plus the reverse-runner client-direction extraction
(`reverse-runner.md`). POC source is disposable, never merged.
- Phase 1 architecture under `docs/architecture/`: the index +
open-questions tracker (OQ-01…13), the spec docs
(`overview.md`, `mapping.md`, `client.md`, `auth.md`), and ADRs
001–006 (Accepted) — the `alk/ssh` ALPN pin; the russh 0.65.0 pin
(aws-lc-rs, no compression, `rsa` off); the capability feature
graph (flat, two-direction, `dynamic-forward` retired); auth
dispositions; vault-key custody; and the mapping/dispatch
decisions (route-1 bridge, exec-only git, parse-only session
dispatch).
+10 -10
View File
@@ -1,6 +1,6 @@
# Single-crate protocol/door crate (the alktty template). No workspace:
# the composition story is deferred — alkssh's substance is the mappings
# (family ADR-003), and phase 0 research settles the russh pin first.
# (family ADR-003); the russh pin is decided (ADR-002: 0.65.0, aws-lc-rs).
[workspace]
members = ["."]
@@ -10,7 +10,7 @@ version = "0.1.0"
edition = "2021"
rust-version = "1.88"
license = "MIT OR Apache-2.0"
description = "SSH adapter for the alk family: russh-wrapped vanilla-ssh termination bridged to/from alkcall channels — session/pty, tunnels, dynamic forwarding, git exec/subsystem — with every capability mapping a fail-closed cargo feature (no features: no capabilities)"
description = "SSH adapter for the alk family: russh-wrapped vanilla-ssh termination bridged to/from alkcall channels — session/pty, tunnels, git exec — with every capability mapping a fail-closed cargo feature (no features: no capabilities)"
repository = "https://git.alk.dev/alkdev/alkssh"
keywords = ["ssh", "russh", "protocol", "alkcall", "network"]
categories = ["network-programming", "asynchronous"]
@@ -24,18 +24,18 @@ exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/rese
name = "alkssh"
[features]
# The capability feature graph (family ADR-003 decision 3) lands with the
# committed architecture: session (→ alktty), tunnel (→ alktunnels),
# dynamic-forward (→ alksocks), git (→ alkgit), sftp (→ alkfs, deferred).
# The default build carries no capabilities by design — every mapping is
# opt-in and every unfeatured channel open declines fail-closed.
# The capability feature graph (ADR-003) lands with implementation start:
# session (→ alktty), tunnel (→ alktunnels), git (→ alkgit), sftp (→
# alkfs, deferred placeholder). The default build carries no capabilities
# by design — every mapping is opt-in and every unfeatured channel open
# declines fail-closed.
default = []
[dependencies]
tokio = { version = "1.54.1", features = ["full"] }
# Deliberately empty at initialize time — the russh version pin is phase 0
# research output (alknet main's `russh = "0.49"` is the reference, not the
# answer). alkcall + sibling-crate deps land with the committed architecture.
# Deliberately empty until implementation start — the russh pin is decided
# (ADR-002: 0.65.0, aws-lc-rs); russh + alkcall + sibling-crate deps land
# with implementation.
[dev-dependencies]
+9 -4
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-26
last_updated: 2026-10-27
---
# Architecture index
@@ -39,8 +39,10 @@ architecture review pass is next, after which specs go to `reviewed`.
## Open Questions
Tracked in [open-questions.md](open-questions.md) — OQ-01 … OQ-13,
seeded from the phase 0 POC findings and research docs. Five resolved
(ADR-002/006 review + evidence), four deferred with named blockers.
seeded from the phase 0 POC findings and research docs. Six resolved
(OQ-02/03/05/06/07/12), three open (OQ-08/10/11 — mapping-detail
decisions in the current review pass), four deferred with named
blockers (OQ-01/04/09/13).
## Document Lifecycle
@@ -58,4 +60,7 @@ seeded from the phase 0 POC findings and research docs. Five resolved
inventory, auth bridge, mapping table, reverse-runner extraction)
- Family: `/workspace/@alkdev/alkfamily/docs/architecture/decisions/001..003`
(ADR-003 is the alkssh structural decision)
- Sibling: alkcall ADRs 002/004/009/034/042/051, alkhttp ADR-014/016/073-analogs
- Sibling: alkcall ADRs 002/003/004/006/009/010/034/042/051 (ALPN
dispatch, auth surface, channels wire/relay); alkhttp ADR-014/073
(secret-material flow and door-surface posture — the patterns
ADR-005 and `auth.md` mirror)
+20 -13
View File
@@ -77,15 +77,20 @@ Two cases, one seam — the crate is **policy-blind**:
2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no anchor
until contact): the crate surfaces
(host, port, presented fingerprint) to an **assembly-provided
`HostKeyPolicy`** — prompt-and-record, provider-metadata match, or
learn-and-record. The OpenSSH "add to known_hosts?" moment becomes
`HostKeyPolicy`** — prompt-and-record, provider-metadata match,
learn-and-record, or accept-and-continue (the ephemeral-fleet
shape). The OpenSSH "add to known_hosts?" moment becomes
assembly policy.
Crate rules: default policy is fail-closed (reject unknown); the
crate never decides trust; accept-all is never a crate option
(alkminer's accept-all is the recorded anti-pattern; for ephemeral
fleets the *real* anchor is hub-held key material — per ADR-005's
custody model — not host pins).
crate never decides trust and never hard-codes a trust model —
accept-all is not a built-in crate option, but a permissive case-2
policy is the assembly's to choose where its fleet model calls for
it (alkminer's accept-all for rental fleets is that shape, made
deliberately: fresh container per rent → fresh host key, so no pin
can exist beforehand; the security anchor there is hub-held
per-instance dial keys — ADR-005's custody model — plus what rides
the tunnels, not host pins).
## What is out of scope
@@ -95,15 +100,16 @@ custody model — not host pins).
OQ-04 deferred on a CA use case).
- ssh-agent integration/forwarding (external signer processes are out
of family scope; the vault is the custody story).
- TOFO/TOFU *as crate behavior* — policy lives at the assembly, the
- TOFU *as crate behavior* — policy lives at the assembly, the
crate is policy-blind (above).
## Open Questions
- **OQ-04** (deferred — certificate auth, on a use case).
- **OQ-07** (open — the vault `decrypt` plain-`String` gap; upstream
ask to alkvault; adapter-side `Zeroizing` convention stands
meanwhile; ADR-005 decision 3 is the contract).
- **OQ-07** (resolved at the adapter level — the vault `decrypt`
plain-`String` gap: adapter-side `Zeroizing` wrap is the contract
(ADR-005 decision 3); the vault-side improvement is an upstream
alkvault ask).
- **OQ-05** (resolved — recorded above).
## References
@@ -113,6 +119,7 @@ custody model — not host pins).
- `docs/research/auth-bridge.md` (the surface mapping + host-key
refinement), `poc-1-auth-bridge.md` (§1–4), `poc-2-vault-keys.md`
(§2–5), `reverse-runner.md` (lessons 5–6)
- alkcall ADRs 002/003/006 (identity surface), alkhttp ADR-014/073
analogs (secret flow, one-backend posture), family ADR-002
decision 5 / family ADR-003 decision 2
- alkcall ADRs 002/003/006 (identity surface) and 010 (secret-material
flow — alkhttp ADR-014's alkcall analog), alkhttp ADR-073 (one
backend behind every door surface), family ADR-002 decision 5 /
family ADR-003 decision 2
+7 -2
View File
@@ -90,7 +90,9 @@ half-capability, which the family forbids (ADR-003 symmetry +
- **Credential storage policy** — assembly (the vault handle is
injected; ADR-005 / OQ-06).
- **Trust decisions** — the host-key policy is assembly-provided
(OQ-05; the crate default rejects).
(OQ-05; the crate default rejects; a permissive case-2 policy for
ephemeral fleets is the assembly's to choose — the crate carries no
trust model of its own).
- **sftp** — until alkfs (OQ-13); the `sftp` feature exists as the
tested decline on both directions.
@@ -118,7 +120,10 @@ half-capability, which the family forbids (ADR-003 symmetry +
pin storage is assembly detail.
- **OQ-12** (resolved): lifecycle primitives only — recorded above.
- **OQ-13** (deferred, alkfs): the sftp client half.
- **OQ-07** (open): rides the vault decrypt zeroization ask (ADR-005).
- **OQ-07** (resolved at the adapter level): the vault `decrypt`
plain-`String` gap — the adapter-side `Zeroizing` wrap is the
contract (ADR-005 decision 3); the vault-side improvement is an
upstream alkvault ask, not alkssh scope.
## References
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-26
last_updated: 2026-10-27
---
# ADR-004: Auth dispositions — publickey-only, fail-closed, fingerprint identity
@@ -65,7 +65,7 @@ twice: which methods exist, and what unauthenticated attempts receive.
`none`-probe: POC-1 measured that the probe consumes one attempt,
so a default of `10` leaves `IdentitiesOnly`-style clients at
least nine real key attempts; a small limit eats one attempt on
every connection (recorded for ADR-008's config defaults).
every connection (recorded for the serve-config defaults).
Constant-time rejection is inherited from
`Config::auth_rejection_time` (russh-managed).
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-26
last_updated: 2026-10-27
---
# ADR-005: At-rest key custody — vault-wrapped OpenSSH-PEM, decrypt-at-use
@@ -116,6 +116,7 @@ both ends).
- `docs/research/poc-2-vault-keys.md` (§1 the 4-way table, §2 the
lifecycle + zeroization bounds, §3 the failure matrix, §4 the
Signer dismissal, §5 host keys end to end)
- alkhttp ADR-014 + ADR-016-analog (the secret-material-flow family pattern)
- alkcall ADR-010 + alkhttp ADR-014 (the secret-material-flow family
pattern — ADR-010 is alkhttp ADR-014's alkcall analog)
- Family ADR-002 decision 5, family ADR-003 decision 2
- OQ-05, OQ-06, OQ-07, OQ-13
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-26
last_updated: 2026-10-27
---
# ADR-006: Mapping decisions — route-1 bridge, exec-only git, session dispatch
@@ -26,7 +26,7 @@ SOCKS-in-channel is feasible if ever wanted back.
pass: which of alktunnels' two routes serves `direct-tcpip`; whether
git over SSH has a subsystem path; and the session-channel dispatch
shape when one channel type serves both parseable commands and
SOCK5-in-channel traffic. POC-3 resolved all three by construction
SOCKS5-in-channel traffic. POC-3 resolved all three by construction
(real git 2.43, real curl 8.5, real OpenSSH through published
alkgit/alksocks/alktunnels/alktty).
+2 -2
View File
@@ -34,7 +34,7 @@ bridge primitives every mapping uses (POC-1 §7).
| `session` → `exec_request` git | `git clone/push ssh://` | `git` | alkgit: parse command → `Service` + repo → `serve_duplex_resolved(repo_id, identity, service, duplex, …)` | **exec-only** (ADR-006 decision 2): the repo rides inside the exec command string (`git-upload-pack '/repo'`); vanilla git never speaks subsystem for git; process-mode dialect (server-speaks-first advertisement); exit-status proven both directions |
| `session` → `subsystem_request` | `sftp` clients; manual `ssh -s` | `sftp` (placeholder) / `git` rows | — | **`sftp` name**: declines until alkfs lands (OQ-13) — the feature exists, the route doesn't. **git service names**: decline with reason — a subsystem request carries only a name, never a repo id (POC-3 §A.3); never half-serves |
| `direct-tcpip` | `ssh -L`; all `ssh -D` traffic | `tunnel` | alktunnels **route 1**: adapter-side bridge — `TunnelSession::open_direct` on a held `ChannelClient`, pump `take_halves()` vs the channel | target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix; accept only **after** the fabric open succeeds — failure maps to an explicit `ConnectFailed` reject (POC-3 §C.2); vanilla `-D`'s dynamic targets dispatch here (POC-3 correction 2) |
| `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable`'s `ForwardedHandler` shape — hand the channel duplex to the handler | listen flow = the global-request rows below; *the* data leg untested in POC-3 beyond refusal (OQ-… note below) |
| `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable`'s `ForwardedHandler` shape — hand the channel duplex to the handler | listen flow = the global-request rows below; the data leg is untested in POC-3 beyond refusal (see the POC-3 note below) |
| `direct-streamlocal` | unix-socket forward | `tunnel` | `SubstrateAddr::Path` treatment | unix-only; decline shape per OQ-11 |
| `x11` | X forwarding | — (never) | — | always declines; not feature-gated (ADR-003 §5) |
| `auth-agent` | agent forwarding | — (never) | — | always declines; the anti-pattern of the alkvault custody posture (ADR-003 §5) |
@@ -76,7 +76,7 @@ the served listen flow is implementation-phase contract-test territory.*
| `signal` | signal control (alktty names verbatim); PTY mode reaches the process group, pipe mode is pid-only (backend semantics, recorded in OQ-10) |
| data / `extended_data(1)` | the translation bridge (rows table) |
| `exit_status` | from the alktty exit chunk — sent **before** close, from the spawned task via `Session::handle()` (POC-3 §D: proven both legs) |
| `exit_signal` | only for signal-terminated exits; OQ-10 (chunk carries no signal name — alktt-y ask + assembly fallback pending) |
| `exit_signal` | only for signal-terminated exits; OQ-10 (chunk carries no signal name — alktty ask + assembly fallback pending) |
## The structural constraint: spawn-and-return
+5 -6
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-26
last_updated: 2026-10-27
---
# Open questions
@@ -25,10 +25,10 @@ alkcall trackers have their own OQ spaces).
## Theme B — session-channel dispatch & mapping details
### OQ-02: ~~What is the session-channel dispatch shape (SOCKS-detect vs parse, and who bounds the window)?~~ — resolved (then simplified)
### OQ-02: ~~What is the session-channel dispatch shape (SOCKS-detect vs parse, and who bounds the window)?~~ — resolved
- **Origin**: [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §Stage B.3, `mapping-table.md` §notes
- **Status**: resolved — resolved twice over. First by ADR-006 point 3 (dispatch composition: request-driven parse wins, request-less channels get the detect). Then **simplified by the ADR-003 §1a amendment**: the `dynamic-forward` feature retired (no consumer is ever the family-native SOCKS-in-channel client; vanilla `-D` is client-side SOCKS + plain `direct-tcpip`; the family `-D` door is alksocks composed locally at the assembly), so the detect lost its owner. Final shape: **session dispatch is parse-only**; request-less session channels decline. POC-3 stage B survives as the feasibility record for re-adding a remote SOCKS door via explicit ADR.
- **Status**: resolved. Decided by ADR-006 point 3, then simplified by the ADR-003 §1a amendment: the `dynamic-forward` feature retired (no consumer is ever the family-native SOCKS-in-channel client; vanilla `-D` is client-side SOCKS + plain `direct-tcpip`; the family `-D` door is alksocks composed locally at the assembly), so the detect lost its owner. **Session dispatch is parse-only**; request-less session channels decline. POC-3 stage B survives as the feasibility record for re-adding a remote SOCKS door via explicit ADR.
- **Cross-references**: ADR-006, ADR-003 §1a
### OQ-03: ~~Username disposition~~ — resolved
@@ -56,9 +56,9 @@ alkcall trackers have their own OQ spaces).
- **Resolution**: two basic client cases, one seam:
1. **Known beforehand** — trust anchors exist. Family-built runners mint their host key at assembly time, so the expected fingerprint is known before first contact: assembly-supplied pins, **hard decline on mismatch** (no soft failure).
2. **Unknown beforehand** — vanilla `sshd` on container fleets the assembly didn't provision; no anchor exists until first contact. The crate surfaces the presented host key (host, port, fingerprint) to an **assembly-provided `HostKeyPolicy`** that decides: prompt-and-record, provider-metadata match, or learn-and-record. The OpenSSH "add to known_hosts?" moment becomes an assembly policy rather than crate behavior.
2. **Unknown beforehand** — vanilla `sshd` on container fleets the assembly didn't provision; no anchor exists until first contact. The crate surfaces the presented host key (host, port, fingerprint) to an **assembly-provided `HostKeyPolicy`** that decides: prompt-and-record, provider-metadata match, learn-and-record, or accept-and-continue (the ephemeral-fleet shape — see below). The OpenSSH "add to known_hosts?" moment becomes an assembly policy rather than crate behavior.
Crate rules that make this family-shaped: the crate's default policy is **fail-closed** (reject unknown, hard-decline mismatch — russh's own reject-all default, kept); the crate never decides trust itself; accept-all is never a crate option (that collapse is the old-code anti-pattern, and for ephemeral fleets strict pinning is the wrong control anyway — the anchor there is hub-held key material/lifecycle, not host pins). The contract lands in the client-side ADR (written with `client.md`; expected to be ADR-007) and the `auth.md` spec.
Crate rules that make this family-shaped: the crate's default policy is **fail-closed** (reject unknown, hard-decline mismatch — russh's own reject-all default, kept); the crate never decides trust itself and never hard-codes a trust model — accept-all is not a built-in crate option, but a permissive case-2 policy is the assembly's to choose where its fleet model calls for it (alkminer's accept-all for rental fleets is that shape, made deliberately: fresh container per rent → fresh host key, so no pin can exist beforehand and the security anchor is hub-held per-instance dial keys plus what rides the tunnels, not host pins). The contract lands in the `auth.md` spec (host-key policy section) and `client.md`'s dial surface.
- **Cross-references**: OQ-07, `reverse-runner.md`, [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) §5 (host keys ride the vault model)
### OQ-06: ~~One vault, many roles?~~ — resolved
@@ -135,7 +135,6 @@ alkcall trackers have their own OQ spaces).
| OQ | Blocked on | Priority |
|---|---|---|
| OQ-01 | concrete relay/browser-leg consumer | medium |
| OQ-02 | (deferred(unclear)) investigation during `mapping.md` | high |
| OQ-04 | concrete OpenSSH-CA use case | low |
| OQ-09 | concrete wire-env use case | low |
| OQ-13 | alkfs landing (family ADR-002) | low (for now) |
+2 -1
View File
@@ -2,7 +2,8 @@
Status: Draft (phase 0 research). Sources: alkcall 0.10.1 source
(`src/core/auth.rs`, `src/core/types.rs`, `src/protocol/dispatch.rs`),
alkcall ADRs 002/003/006/010/014-analog (alkhttp ADR-014), alkvault
alkcall ADRs 002/003/006/010 (ADR-010 is alkhttp ADR-014's alkcall
analog), alkvault
0.1.0 source, alkhttp architecture docs.
## The server side: keys in, identity out
+9 -7
View File
@@ -125,11 +125,10 @@ At-rest hygiene, verified end to end:
The wrong-mnemonic/tampered cases land in
`VaultServiceError::Encryption` *as strings* — the error taxonomy at
the adapter is ours to shape (distinct variants), but the root causes
are distinguishable today via the variant + message. The `InvalidPath`
behavior for version < 2 is deliberate (v1 is the TS_PBKDF2 legacy the
are distinguishable today via the variant + message. The `InvalidPath` behavior for version < 2 is deliberate (v1 is the TS_PBKDF2 legacy the
vault cannot decrypt; rotation re-encryption is the migration path,
per alkvault's own docs) — consistent with the ADR-021-style
version-indexed derivation the alkssh ADR will cite for rotation.
per alkvault's own docs) — consistent with alkvault's ADR-021
version-indexed rotation model.
### 4. Signer-seam dismissal — confirmed, now with runtime and source evidence
@@ -265,9 +264,12 @@ with evidence". Evidence assembled:
2. **Key-record schema**: alkssh's vault-backed key record should treat
`EncryptedData` as opaque and never interpret `key_version` itself
(the vault owns version semantics; `InvalidPath` for unknown
versions is the fail-closed behavior). Rotation (at-rest version
bump, key constant) is a storage-side operation — proven to
preserve key identity.
`InvalidPath`
for unknown
versions is the fail-closed behavior). Rotation (at-rest version
bump, key constant) is a storage-side operation — proven to
preserve key identity (consistent with alkvault's own
version-indexed rotation decision, alkvault ADR-021).
3. **One vault, many roles?** The POC held client dial keys *and* the
server host key in one vault (one mnemonic). Whether the assembly
layer should run separate vault instances per role (client vs
+2 -1
View File
@@ -243,7 +243,8 @@ construct the mapping table names — then runs the real session. Proven
against a **real PTY** (`LocalTtyBackend` over portable-pty):
- `pty_request` args → `TerminalParamsWire` (`term`/cols/rows/
pixels; SSH modes → the reserved `{}` OQ-44 JSON) — live: bash on
pixels; SSH modes → the reserved `{}` JSON — an alktty-side
reservation, alktty OQ-44; modes are deferred there) — live: bash on
the PTY answered `stty size` with the requested 24×80.
- Keystrokes ride in, PTY echo and shell output ride out (real bash
prompt captured in the run output).
+20 -19
View File
@@ -7,8 +7,8 @@ facts). Source:
`/workspace/@alkminer/alkminer` — specifically
`crates/alkminer-host/src/remote/{ssh,exec,forward,manager,sftp}.rs`
and `src/oai/mod.rs`. Also old (russh 0.62 era) and POC-grade; its
anti-patterns are as instructive as its patterns, and are named below
as anti-patterns, not precedents.
weak spots are as instructive as its strengths, and both are recorded
below as facts to design from, not precedents to copy.
## The direction
@@ -75,21 +75,22 @@ One authenticated SSH connection per instance carried:
assembly policy (session/forward lifecycle), not wire contract —
kept in view so phase 1 doesn't accidentally bake reconnect policy
into the crate.
5. **Host-key pinning anti-pattern.** alkminer's `check_server_key`
returned accept-all (TOFU-without-the-F, 0.62 default posture
overridden to the permissive extreme for rental fleets). The family
rule: no trust decisions inside the crate — default fail-closed,
trust via an assembly-provided policy seam. The lesson is sharper
than "pin everything", though: for *ephemeral* fleets strict TOFU
is unusable anyway (fresh container each rent → fresh host key;
the churn is exactly why accept-all won), so the real security
anchor there is hub-held key material (per-instance dial keys make
a captured hub credential worthless) plus what rides the tunnels;
host pinning is the right control only where anchors can exist
(family-built runners, whose host key the assembly mints and
therefore knows beforehand). The fleet churn is the reason the
two-case policy seam (OQ-05) is the correct shape rather than
"pin everything".
5. **Host-key acceptance for ephemeral fleets — the pattern the fleet
model demands.** alkminer's `check_server_key` returned accept-all
for rental fleets. That was the correct pattern for that problem,
not a lapse: a fresh container per rent mints a fresh host key, so
no pin can exist beforehand — strict TOFU/"pin everything" is
unusable there, and the churn is exactly why accept-all won. The
security anchor is elsewhere: hub-held per-instance dial keys make
a captured hub credential worthless, plus what rides the tunnels.
The family rule is therefore not "pin everything" — it is "no
trust decisions inside the crate": default fail-closed at the
crate, with an assembly-provided policy seam so each fleet model
gets the control that fits it — host pinning where anchors can
exist (family-built runners, whose host key the assembly mints and
therefore knows beforehand), permissive accept where they can't
(rental fleets). The fleet churn is the reason the two-case policy
seam (OQ-05) is the correct shape rather than "pin everything".
6. **Per-runner key registration.** Keys were minted per instance and
registered hub-side (their provisioning flow, including a real
gitea client for dev workspaces) — the deployment precedent for
@@ -112,6 +113,6 @@ One authenticated SSH connection per instance carried:
server side of the same bridge legs), `poc-1-auth-bridge.md` §7
(the channel↔stream plumbing both leg styles reuse),
`poc-3-mapping-bakeoff.md` §D.3 (client-request mechanics)
- `auth-bridge.md` §Host keys and `check_server_key` (the pinning
rule this doc's anti-pattern motivates)
- `auth-bridge.md` §Host keys and `check_server_key` (the trust-policy
seam this doc's ephemeral-fleet acceptance pattern motivates)
- alkminer ADR-016 (the OAI proxy pattern — secrets stay at the hub)
+15 -14
View File
@@ -4,19 +4,20 @@
//!
//! No SSH framing is authored in-family (family ADR-003): russh owns the
//! wire, and the crate's substance is the channel-type mappings —
//! session/pty → alktty, direct/forwarded-tcpip → alktunnels, dynamic
//! forwarding (`-D`) → alksocks, git exec/subsystem → alkgit, sftp →
//! alkfs later. Every mapping is a cargo feature; a session opening an
//! unfeatured or unimplemented channel type is declined with a clean
//! error — never a half-capability, never a silent fallback. The default
//! build (no features) is structurally incapable of serving anything but
//! declines.
//! session/pty → alktty, direct/forwarded-tcpip → alktunnels, git exec →
//! alkgit, sftp → alkfs later (a declining placeholder until then), each
//! behind its cargo feature. Git over SSH is exec-only (ADR-006): a
//! subsystem request can never carry the repository, so it declines. A
//! client opening an unfeatured or unimplemented channel type gets a
//! clean decline — never a half-capability, never a silent fallback; the
//! default build (no features) is structurally incapable of serving
//! anything but declines.
//!
//! Keys reuse the alknet `main` key model (`russh::keys`,
//! authorized-key/KeySource handling) via alkvault-backed sources — never
//! env vars.
//! Identity is the family's, not SSH's: a verified key's fingerprint →
//! alkcall `IdentityProvider`. Keys the crate holds (client dial keys,
//! host keys) come from alkvault-backed sources — vault-encrypted at
//! rest, decrypted at use (ADR-005), never env vars.
//!
//! This crate is initialized (docs only, SDD phase 0): the dependency
//! surface, the feature graph, and the mapping table are phase 0 research
//! output and land with the committed architecture. See
//! `docs/research/` and family ADR-003.
//! Architecture: `docs/architecture/` (phase 1 — ADRs 001–006 accepted;
//! the dependency surface and feature graph land with implementation
//! start). See also `docs/research/` and family ADR-003.