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:
1 parent
fe9f97b71d
commit
263f91fdb7
15 files changed
+124
-86
No files matched your search
+16
-1
@@ -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
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
@@ -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.
|
|
||||||
Reference in new issue
Block a user