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 `auth-bridge.md` (fingerprint → identity server path; vault-backed
at-rest client key storage — the vault as secret store, not key at-rest client key storage — the vault as secret store, not key
derivation), `mapping-table.md` (channel type/request → family seam 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: # Single-crate protocol/door crate (the alktty template). No workspace:
# the composition story is deferred — alkssh's substance is the mappings # 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] [workspace]
members = ["."] members = ["."]
@@ -10,7 +10,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
rust-version = "1.88" rust-version = "1.88"
license = "MIT OR Apache-2.0" 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" repository = "https://git.alk.dev/alkdev/alkssh"
keywords = ["ssh", "russh", "protocol", "alkcall", "network"] keywords = ["ssh", "russh", "protocol", "alkcall", "network"]
categories = ["network-programming", "asynchronous"] categories = ["network-programming", "asynchronous"]
@@ -24,18 +24,18 @@ exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/rese
name = "alkssh" name = "alkssh"
[features] [features]
# The capability feature graph (family ADR-003 decision 3) lands with the # The capability feature graph (ADR-003) lands with implementation start:
# committed architecture: session (→ alktty), tunnel (→ alktunnels), # session (→ alktty), tunnel (→ alktunnels), git (→ alkgit), sftp (→
# dynamic-forward (→ alksocks), git (→ alkgit), sftp (→ alkfs, deferred). # alkfs, deferred placeholder). The default build carries no capabilities
# The default build carries no capabilities by design — every mapping is # by design — every mapping is opt-in and every unfeatured channel open
# opt-in and every unfeatured channel open declines fail-closed. # declines fail-closed.
default = [] default = []
[dependencies] [dependencies]
tokio = { version = "1.54.1", features = ["full"] } tokio = { version = "1.54.1", features = ["full"] }
# Deliberately empty at initialize time — the russh version pin is phase 0 # Deliberately empty until implementation start — the russh pin is decided
# research output (alknet main's `russh = "0.49"` is the reference, not the # (ADR-002: 0.65.0, aws-lc-rs); russh + alkcall + sibling-crate deps land
# answer). alkcall + sibling-crate deps land with the committed architecture. # with implementation.
[dev-dependencies] [dev-dependencies]
+9 -4
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-26 last_updated: 2026-10-27
--- ---
# Architecture index # Architecture index
@@ -39,8 +39,10 @@ architecture review pass is next, after which specs go to `reviewed`.
## Open Questions ## Open Questions
Tracked in [open-questions.md](open-questions.md) — OQ-01 … OQ-13, Tracked in [open-questions.md](open-questions.md) — OQ-01 … OQ-13,
seeded from the phase 0 POC findings and research docs. Five resolved seeded from the phase 0 POC findings and research docs. Six resolved
(ADR-002/006 review + evidence), four deferred with named blockers. (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 ## 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) inventory, auth bridge, mapping table, reverse-runner extraction)
- Family: `/workspace/@alkdev/alkfamily/docs/architecture/decisions/001..003` - Family: `/workspace/@alkdev/alkfamily/docs/architecture/decisions/001..003`
(ADR-003 is the alkssh structural decision) (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 2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no anchor
until contact): the crate surfaces until contact): the crate surfaces
(host, port, presented fingerprint) to an **assembly-provided (host, port, presented fingerprint) to an **assembly-provided
`HostKeyPolicy`** — prompt-and-record, provider-metadata match, or `HostKeyPolicy`** — prompt-and-record, provider-metadata match,
learn-and-record. The OpenSSH "add to known_hosts?" moment becomes learn-and-record, or accept-and-continue (the ephemeral-fleet
shape). The OpenSSH "add to known_hosts?" moment becomes
assembly policy. assembly policy.
Crate rules: default policy is fail-closed (reject unknown); the Crate rules: default policy is fail-closed (reject unknown); the
crate never decides trust; accept-all is never a crate option crate never decides trust and never hard-codes a trust model —
(alkminer's accept-all is the recorded anti-pattern; for ephemeral accept-all is not a built-in crate option, but a permissive case-2
fleets the *real* anchor is hub-held key material — per ADR-005's policy is the assembly's to choose where its fleet model calls for
custody model — not host pins). 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 ## What is out of scope
@@ -95,15 +100,16 @@ custody model — not host pins).
OQ-04 deferred on a CA use case). OQ-04 deferred on a CA use case).
- ssh-agent integration/forwarding (external signer processes are out - ssh-agent integration/forwarding (external signer processes are out
of family scope; the vault is the custody story). 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). crate is policy-blind (above).
## Open Questions ## Open Questions
- **OQ-04** (deferred — certificate auth, on a use case). - **OQ-04** (deferred — certificate auth, on a use case).
- **OQ-07** (open — the vault `decrypt` plain-`String` gap; upstream - **OQ-07** (resolved at the adapter level — the vault `decrypt`
ask to alkvault; adapter-side `Zeroizing` convention stands plain-`String` gap: adapter-side `Zeroizing` wrap is the contract
meanwhile; ADR-005 decision 3 is the contract). (ADR-005 decision 3); the vault-side improvement is an upstream
alkvault ask).
- **OQ-05** (resolved — recorded above). - **OQ-05** (resolved — recorded above).
## References ## References
@@ -113,6 +119,7 @@ custody model — not host pins).
- `docs/research/auth-bridge.md` (the surface mapping + host-key - `docs/research/auth-bridge.md` (the surface mapping + host-key
refinement), `poc-1-auth-bridge.md` (§1–4), `poc-2-vault-keys.md` refinement), `poc-1-auth-bridge.md` (§1–4), `poc-2-vault-keys.md`
(§2–5), `reverse-runner.md` (lessons 5–6) (§2–5), `reverse-runner.md` (lessons 5–6)
- alkcall ADRs 002/003/006 (identity surface), alkhttp ADR-014/073 - alkcall ADRs 002/003/006 (identity surface) and 010 (secret-material
analogs (secret flow, one-backend posture), family ADR-002 flow — alkhttp ADR-014's alkcall analog), alkhttp ADR-073 (one
decision 5 / family ADR-003 decision 2 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 - **Credential storage policy** — assembly (the vault handle is
injected; ADR-005 / OQ-06). injected; ADR-005 / OQ-06).
- **Trust decisions** — the host-key policy is assembly-provided - **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 - **sftp** — until alkfs (OQ-13); the `sftp` feature exists as the
tested decline on both directions. tested decline on both directions.
@@ -118,7 +120,10 @@ half-capability, which the family forbids (ADR-003 symmetry +
pin storage is assembly detail. pin storage is assembly detail.
- **OQ-12** (resolved): lifecycle primitives only — recorded above. - **OQ-12** (resolved): lifecycle primitives only — recorded above.
- **OQ-13** (deferred, alkfs): the sftp client half. - **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 ## References
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-26 last_updated: 2026-10-27
--- ---
# ADR-004: Auth dispositions — publickey-only, fail-closed, fingerprint identity # 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, `none`-probe: POC-1 measured that the probe consumes one attempt,
so a default of `10` leaves `IdentitiesOnly`-style clients at so a default of `10` leaves `IdentitiesOnly`-style clients at
least nine real key attempts; a small limit eats one attempt on 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 Constant-time rejection is inherited from
`Config::auth_rejection_time` (russh-managed). `Config::auth_rejection_time` (russh-managed).
@@ -1,6 +1,6 @@
--- ---
status: draft 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 # 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 - `docs/research/poc-2-vault-keys.md` (§1 the 4-way table, §2 the
lifecycle + zeroization bounds, §3 the failure matrix, §4 the lifecycle + zeroization bounds, §3 the failure matrix, §4 the
Signer dismissal, §5 host keys end to end) 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 - Family ADR-002 decision 5, family ADR-003 decision 2
- OQ-05, OQ-06, OQ-07, OQ-13 - OQ-05, OQ-06, OQ-07, OQ-13
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-26 last_updated: 2026-10-27
--- ---
# ADR-006: Mapping decisions — route-1 bridge, exec-only git, session dispatch # 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 pass: which of alktunnels' two routes serves `direct-tcpip`; whether
git over SSH has a subsystem path; and the session-channel dispatch git over SSH has a subsystem path; and the session-channel dispatch
shape when one channel type serves both parseable commands and 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 (real git 2.43, real curl 8.5, real OpenSSH through published
alkgit/alksocks/alktunnels/alktty). 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` → `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 | | `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) | | `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 | | `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) | | `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) | | `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) | | `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) | | 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_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 ## The structural constraint: spawn-and-return
+5 -6
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-26 last_updated: 2026-10-27
--- ---
# Open questions # Open questions
@@ -25,10 +25,10 @@ alkcall trackers have their own OQ spaces).
## Theme B — session-channel dispatch & mapping details ## 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 - **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 - **Cross-references**: ADR-006, ADR-003 §1a
### OQ-03: ~~Username disposition~~ — resolved ### OQ-03: ~~Username disposition~~ — resolved
@@ -56,9 +56,9 @@ alkcall trackers have their own OQ spaces).
- **Resolution**: two basic client cases, one seam: - **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). 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) - **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 ### OQ-06: ~~One vault, many roles?~~ — resolved
@@ -135,7 +135,6 @@ alkcall trackers have their own OQ spaces).
| OQ | Blocked on | Priority | | OQ | Blocked on | Priority |
|---|---|---| |---|---|---|
| OQ-01 | concrete relay/browser-leg consumer | medium | | 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-04 | concrete OpenSSH-CA use case | low |
| OQ-09 | concrete wire-env use case | low | | OQ-09 | concrete wire-env use case | low |
| OQ-13 | alkfs landing (family ADR-002) | low (for now) | | 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 Status: Draft (phase 0 research). Sources: alkcall 0.10.1 source
(`src/core/auth.rs`, `src/core/types.rs`, `src/protocol/dispatch.rs`), (`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. 0.1.0 source, alkhttp architecture docs.
## The server side: keys in, identity out ## 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 The wrong-mnemonic/tampered cases land in
`VaultServiceError::Encryption` *as strings* — the error taxonomy at `VaultServiceError::Encryption` *as strings* — the error taxonomy at
the adapter is ours to shape (distinct variants), but the root causes the adapter is ours to shape (distinct variants), but the root causes
are distinguishable today via the variant + message. The `InvalidPath` are distinguishable today via the variant + message. The `InvalidPath` behavior for version < 2 is deliberate (v1 is the TS_PBKDF2 legacy the
behavior for version < 2 is deliberate (v1 is the TS_PBKDF2 legacy the
vault cannot decrypt; rotation re-encryption is the migration path, vault cannot decrypt; rotation re-encryption is the migration path,
per alkvault's own docs) — consistent with the ADR-021-style per alkvault's own docs) — consistent with alkvault's ADR-021
version-indexed derivation the alkssh ADR will cite for rotation. version-indexed rotation model.
### 4. Signer-seam dismissal — confirmed, now with runtime and source evidence ### 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 2. **Key-record schema**: alkssh's vault-backed key record should treat
`EncryptedData` as opaque and never interpret `key_version` itself `EncryptedData` as opaque and never interpret `key_version` itself
(the vault owns version semantics; `InvalidPath` for unknown (the vault owns version semantics; `InvalidPath` for unknown
versions is the fail-closed behavior). Rotation (at-rest version `InvalidPath`
bump, key constant) is a storage-side operation — proven to for unknown
preserve key identity. 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 3. **One vault, many roles?** The POC held client dial keys *and* the
server host key in one vault (one mnemonic). Whether the assembly server host key in one vault (one mnemonic). Whether the assembly
layer should run separate vault instances per role (client vs 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): against a **real PTY** (`LocalTtyBackend` over portable-pty):
- `pty_request` args → `TerminalParamsWire` (`term`/cols/rows/ - `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. the PTY answered `stty size` with the requested 24×80.
- Keystrokes ride in, PTY echo and shell output ride out (real bash - Keystrokes ride in, PTY echo and shell output ride out (real bash
prompt captured in the run output). prompt captured in the run output).
+20 -19
View File
@@ -7,8 +7,8 @@ facts). Source:
`/workspace/@alkminer/alkminer` — specifically `/workspace/@alkminer/alkminer` — specifically
`crates/alkminer-host/src/remote/{ssh,exec,forward,manager,sftp}.rs` `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 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 weak spots are as instructive as its strengths, and both are recorded
as anti-patterns, not precedents. below as facts to design from, not precedents to copy.
## The direction ## The direction
@@ -75,21 +75,22 @@ One authenticated SSH connection per instance carried:
assembly policy (session/forward lifecycle), not wire contract — assembly policy (session/forward lifecycle), not wire contract —
kept in view so phase 1 doesn't accidentally bake reconnect policy kept in view so phase 1 doesn't accidentally bake reconnect policy
into the crate. into the crate.
5. **Host-key pinning anti-pattern.** alkminer's `check_server_key` 5. **Host-key acceptance for ephemeral fleets — the pattern the fleet
returned accept-all (TOFU-without-the-F, 0.62 default posture model demands.** alkminer's `check_server_key` returned accept-all
overridden to the permissive extreme for rental fleets). The family for rental fleets. That was the correct pattern for that problem,
rule: no trust decisions inside the crate — default fail-closed, not a lapse: a fresh container per rent mints a fresh host key, so
trust via an assembly-provided policy seam. The lesson is sharper no pin can exist beforehand — strict TOFU/"pin everything" is
than "pin everything", though: for *ephemeral* fleets strict TOFU unusable there, and the churn is exactly why accept-all won. The
is unusable anyway (fresh container each rent → fresh host key; security anchor is elsewhere: hub-held per-instance dial keys make
the churn is exactly why accept-all won), so the real security a captured hub credential worthless, plus what rides the tunnels.
anchor there is hub-held key material (per-instance dial keys make The family rule is therefore not "pin everything" — it is "no
a captured hub credential worthless) plus what rides the tunnels; trust decisions inside the crate": default fail-closed at the
host pinning is the right control only where anchors can exist crate, with an assembly-provided policy seam so each fleet model
(family-built runners, whose host key the assembly mints and gets the control that fits it — host pinning where anchors can
therefore knows beforehand). The fleet churn is the reason the exist (family-built runners, whose host key the assembly mints and
two-case policy seam (OQ-05) is the correct shape rather than therefore knows beforehand), permissive accept where they can't
"pin everything". (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 6. **Per-runner key registration.** Keys were minted per instance and
registered hub-side (their provisioning flow, including a real registered hub-side (their provisioning flow, including a real
gitea client for dev workspaces) — the deployment precedent for 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 server side of the same bridge legs), `poc-1-auth-bridge.md` §7
(the channel↔stream plumbing both leg styles reuse), (the channel↔stream plumbing both leg styles reuse),
`poc-3-mapping-bakeoff.md` §D.3 (client-request mechanics) `poc-3-mapping-bakeoff.md` §D.3 (client-request mechanics)
- `auth-bridge.md` §Host keys and `check_server_key` (the pinning - `auth-bridge.md` §Host keys and `check_server_key` (the trust-policy
rule this doc's anti-pattern motivates) seam this doc's ephemeral-fleet acceptance pattern motivates)
- alkminer ADR-016 (the OAI proxy pattern — secrets stay at the hub) - 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 //! No SSH framing is authored in-family (family ADR-003): russh owns the
//! wire, and the crate's substance is the channel-type mappings — //! wire, and the crate's substance is the channel-type mappings —
//! session/pty → alktty, direct/forwarded-tcpip → alktunnels, dynamic //! session/pty → alktty, direct/forwarded-tcpip → alktunnels, git exec →
//! forwarding (`-D`) → alksocks, git exec/subsystem → alkgit, sftp → //! alkgit, sftp → alkfs later (a declining placeholder until then), each
//! alkfs later. Every mapping is a cargo feature; a session opening an //! behind its cargo feature. Git over SSH is exec-only (ADR-006): a
//! unfeatured or unimplemented channel type is declined with a clean //! subsystem request can never carry the repository, so it declines. A
//! error — never a half-capability, never a silent fallback. The default //! client opening an unfeatured or unimplemented channel type gets a
//! build (no features) is structurally incapable of serving anything but //! clean decline — never a half-capability, never a silent fallback; the
//! declines. //! default build (no features) is structurally incapable of serving
//! anything but declines.
//! //!
//! Keys reuse the alknet `main` key model (`russh::keys`, //! Identity is the family's, not SSH's: a verified key's fingerprint →
//! authorized-key/KeySource handling) via alkvault-backed sources — never //! alkcall `IdentityProvider`. Keys the crate holds (client dial keys,
//! env vars. //! 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 //! Architecture: `docs/architecture/` (phase 1 — ADRs 001–006 accepted;
//! surface, the feature graph, and the mapping table are phase 0 research //! the dependency surface and feature graph land with implementation
//! output and land with the committed architecture. See //! start). See also `docs/research/` and family ADR-003.
//! `docs/research/` and family ADR-003.