- pocs.md: marked complete; corrected the stale 0.5x-era accept-defaults hypothesis wording (0.64.1: only auth_publickey_offered defaults to accept), stage D recorded as ran-in-full, completion gate updated with per-question resolution status - added client-side scope section: no dedicated POC needed (client API exercised throughout POC-1/POC-3) + the acceptor-default fact (russh client accept-and-drop for unhandled forwarded opens is ours to fix) - new reverse-runner.md: alkminer pattern extraction (concepts only, never code) — hub-dials-runner surface inventory, both tunnel legs, the OAI-proxy secret-hygiene shape, and the client-direction lessons (keepalive defaults, reconnect-restore, acceptor disposition) - vision.md: scope-question resolution addendum + whitespace typo fix
201 lines
10 KiB
Markdown
201 lines
10 KiB
Markdown
# POCs — phase 0 validation plan
|
||
|
||
Status: **Complete** (phase 0). All three POCs ran against the current
|
||
published russh (0.64.1) and passed; the findings docs
|
||
(`poc-1-auth-bridge.md`, `poc-2-vault-keys.md`,
|
||
`poc-3-mapping-bakeoff.md`) are canonical and supersede the planned
|
||
hypothesis wording below wherever they record corrections. POC source
|
||
is disposable and never merges to main. Mode for all: **standalone**
|
||
(repos have no code to build on yet) — scratch cargo projects at
|
||
`/workspace/<poc-name>` unless a POC explicitly builds on repo code
|
||
(then branch mode per `docs/sdd_process.md`).
|
||
|
||
The hypotheses below are what phase 1's ADRs will cite. If a POC
|
||
refutes a hypothesis, the finding doc records the refutation — that is
|
||
the point of running it.
|
||
|
||
## POC-1 — auth bridge + fail-closed declines end to end
|
||
|
||
Findings: `poc-1-auth-bridge.md` (complete — 31/31; all hypotheses
|
||
confirmed, one correction to the research docs' russh default-impl
|
||
assumption). POC source: `/workspace/alkssh-poc1`.
|
||
|
||
**Question:** does the russh 0.64-era server auth surface compose with
|
||
alkcall's fingerprint-based identity model as cleanly as
|
||
`auth-bridge.md` assumes, and does the fail-closed decline contract
|
||
hold under a real vanilla client?
|
||
|
||
**Construction:** standalone crate. russh server loopback bound to a
|
||
local listener + vanilla `ssh` (OpenSSH) as the client (the point is
|
||
to validate against the *vanilla* wire, not just russh's own client).
|
||
A config-backed `IdentityProvider` stub registered with two ed25519
|
||
keys; russh client available as a secondary client for
|
||
`Signer`-shaped checks.
|
||
|
||
**Validates:**
|
||
- `auth_publickey` after russh's signature verification →
|
||
`ssh_key` fingerprint (`HashAlg::Sha256`) string parity with a
|
||
stub-registered identity; `resolve_from_fingerprint` → `Some` →
|
||
authenticated; unknown key → clean decline under
|
||
`auth_rejection_time`/`max_auth_attempts`.
|
||
- `auth_publickey_offered` disposition (accept-and-let-decide) behaves
|
||
with OpenSSH's probe-then-sign flow.
|
||
- Decline matrix: `auth_none`/`password`/`keyboard-interactive`
|
||
declined (0.64.1 correction: the *default impls already reject* at
|
||
this russh — only `auth_publickey_offered` defaults to accept;
|
||
`poc-1-auth-bridge.md` §3. The family contract still requires
|
||
implementing every hook explicitly — the pin is a one-way door);
|
||
`Config::methods` advertises `publickey` only.
|
||
- `ChannelOpenHandle` drop = auto-reject semantics; unfeatured
|
||
channel-type declines observable by vanilla `ssh`
|
||
(`ssh host` session-open refused, `-L` refused, clean diagnostics).
|
||
- `Channel` ↔ AsyncRead/AsyncWrite conversion (`into_stream`/`split`)
|
||
behaves under a trivial echo (the plumbing POC-2 leans on).
|
||
|
||
**Hypothesized outcome:** the auth-bridge design holds; the only
|
||
uncertainty is fingerprint-format parity details, which this either
|
||
confirms or corrects.
|
||
|
||
## POC-2 — client-side at-rest key handling (vault-backed storage)
|
||
|
||
Findings: `poc-2-vault-keys.md` (complete — 39/39; OpenSSH-PEM-inside-
|
||
`EncryptedData` confirmed, Signer seam dismissed with evidence).
|
||
|
||
**Question:** how are held SSH keys stored and used — the vault as
|
||
pure at-rest encryption (the key is just another bearer-token-class
|
||
secret: opaque bytes, vault-encrypted at rest, decrypted at use), what
|
||
byte format sits inside, and how does the decrypt→dial lifecycle
|
||
behave?
|
||
|
||
**Construction:** standalone. alkvault `encrypt`/`decrypt`
|
||
(`EncryptedData`) wrapping an OpenSSH-encoded private key (generated
|
||
in-POC) → `ssh-key` `decode_secret_key` → `russh::keys::PrivateKey` →
|
||
`authenticate_publickey` against POC-1's server loopback (which resolves
|
||
the key's fingerprint through the same stub `IdentityProvider`).
|
||
|
||
**Validates:**
|
||
- The at-rest shape: OpenSSH private-key encoding as the inner format
|
||
vs raw seed bytes or PKCS#8 (the ssh-key crate's own encrypted-key
|
||
format with a passphrase is an alternative wrapper worth comparing
|
||
against alkvault's outer encryption).
|
||
- The at-use lifecycle: decrypt-at-dial → one-time `decode_secret_key`
|
||
→ zeroized handling for the dial's lifetime; what happens locked /
|
||
absent (fail-closed no-dial) / mid-session unlock rotation.
|
||
- Whether a `Signer`-shaped seam is still worth anything given the
|
||
bytes are handled client-side at use anyway (expected answer: no —
|
||
recorded so the ADR can dismiss it with evidence).
|
||
- Server and client host-key material riding the same model.
|
||
|
||
**Scope note:** the vault is for secrets *we* hold — client-side
|
||
outgoing keys and the server's own host key — and is explicitly not
|
||
involved in resolving *clients'* fingerprints (that path is the
|
||
assembly-layer `IdentityProvider`). The vault's derivation surface
|
||
(`derive_ed25519`, SLIP-0010 paths) is not on this path either — that
|
||
is a different alkvault feature, not SSH key custody. Kept as its own
|
||
POC because its failure modes (at-rest/lifecycle, zeroization, lock
|
||
behavior) are unrelated to the auth-flow failure modes of POC-1.
|
||
|
||
**Hypothesized outcome:** OpenSSH-encoded key inside alkvault
|
||
`EncryptedData`, one-time `decode_secret_key` at dial, zeroizing
|
||
wrapper — confirming the alkhttp ADR-014 analogy end to end.
|
||
|
||
## POC-3 — the mapping bake-off: channel bridge through a real sibling seam
|
||
|
||
Findings: `poc-3-mapping-bakeoff.md` (complete — 51/51 across stages
|
||
A–D; every hypothesis confirmed, three mapping-table corrections
|
||
recorded, plus the russh inline-dispatch rule). POC source:
|
||
`/workspace/alkssh-poc3`.
|
||
|
||
**Question:** does "russh channel duplex → family serve-over-a-stream
|
||
entry" actually work against the real published seams — and which of
|
||
the mapping-table's route choices survives contact with reality?
|
||
|
||
**Construction:** standalone, but *depending on the real sibling
|
||
crates* (alktty/alksocks/alkgit + alkcall types via their published
|
||
forms). russh server loopback + vanilla clients; per stage:
|
||
|
||
- **Stage A — git exec mapping** (the alkforge-critical path): vanilla
|
||
`git clone/push` over `ssh://` → russh server → parse exec command →
|
||
`serve_duplex_resolved(...)` over the split channel halves with stub
|
||
`GitRefs`/`GitPackGen`/`GitPackIngest` (or the substrate composition
|
||
if a real repo is wanted late in the POC). Confirms: the Parse-mode
|
||
(server-speaks-first advertisement) dialect satisfies vanilla git's
|
||
expectations over exec; channel EOF/close maps to session
|
||
termination; exit-status flows.
|
||
- **Stage B — `-D` mapping**: russh server, feature `dynamic-forward`-
|
||
shaped build → `serve_socks5_over` over a session channel → `curl`
|
||
through the SOCKS5 proxy to a local HTTP endpoint. Confirms: the
|
||
dispatch-split inside `session` (parse-then-route vs SOCKS-detect),
|
||
duplex backpressure under throughput.
|
||
- **Stage C — `-L` mapping**: `direct-tcpip` → the mapping-table's two
|
||
alktunnels routes (adapter-side `TunnelSession::open_direct` via a
|
||
real `ChannelClient` vs local-dial stand-in). Whichever route the
|
||
POC exercises informs the phase 1 ADR on the presence/absence of a
|
||
serve-over-stream entry in alktunnels.
|
||
- **Stage D — session/pty mapping**, final stage (ran in full): shell,
|
||
exec, window-change and signal mapping against a real PTY
|
||
(`LocalTtyBackend`, alktty `local`). The "optional" hedge in the
|
||
original plan was unnecessary — stage D completed 13/13 and produced
|
||
the load-bearing session-row corrections: the alktty bridge is a
|
||
**translation bridge** (chunk framing both directions, not a byte
|
||
pump) and the alktty exit chunk carries no signal identity
|
||
(`poc-3-mapping-bakeoff.md` §Stage D).
|
||
|
||
**Validates:** the one-seam-per-mapping hypothesis; the parse-then-
|
||
route dispatch; stderr semantics in pipe vs PTY mode; EOF/close/signal
|
||
fidelity end to end; throughput sanity (window sizes vs the 1 MiB
|
||
channels reassembly cap on the bridged route).
|
||
|
||
**Hypothesized outcome:** stages A–C confirm the design with local
|
||
fixups recorded; stage D may surface PTY-merge stderr surprises that
|
||
reshape the request-level mapping rows.
|
||
|
||
## Client-side scope (post-POC addition)
|
||
|
||
The three POCs are server-door-centric by design, but the crate has a
|
||
**client direction** of equal scope: a family app dials an *upstream*
|
||
vanilla-SSH server and consumes its surface (exec/pty channels,
|
||
`direct-tcpip` opens, the `tcpip_forward` + accepted-`forwarded-tcpip`
|
||
listener flow). No dedicated POC was needed: the POCs exercised the
|
||
client API throughout (POC-1's russh-client auth leg, POC-3 §D.3's
|
||
client-side request mechanics — `Channel::exec()`, the `wait()`-drain
|
||
trap, fire-and-forget `window_change`/`signal` before `into_stream()`).
|
||
|
||
Two client-side facts phase 1 designs from:
|
||
|
||
1. **The acceptor rule.** The only server-initiated channel type in the
|
||
pattern is `forwarded-tcpip`, and russh's client default for an
|
||
unhandled forwarded open is *accept-and-drop* — which is not the
|
||
family fail-closed posture. The client-side accepted-open
|
||
disposition (accept into a registered handler vs decline) is
|
||
alkssh's contract to define.
|
||
2. **A real consumer pattern exists**: the alkminer hub dialing rented
|
||
GPU containers (exec + job management, SFTP binary deploy, `-L` and
|
||
`-R` tunnel legs in both directions, keepalives tuned for NAT'd
|
||
hosts). Extracted (concepts only, never code) in
|
||
`reverse-runner.md`.
|
||
|
||
## Sequencing and gates
|
||
|
||
1. POC-1 first — everything downstream presumes its auth plumbing.
|
||
2. POC-2 after POC-1 (reuses POC-1's server loopback).
|
||
3. POC-3 last; stage A gates phase 1's mapping ADR work — it is the
|
||
path alkforge phase 0 consumes first.
|
||
4. Phase 0 completed when the findings docs existed and the OQ list in
|
||
`vision.md` §scope questions was either answered by findings or
|
||
carried into phase 1's `open-questions.md` as seeded OQs. Done:
|
||
POC-1/2/3 findings docs exist; scope question 5 (mapping-table
|
||
details) is answered per `poc-3-mapping-bakeoff.md` §hypothesis
|
||
verdicts; scope question 4 (auth dispositions + client-side vault
|
||
keys) is answered per POC-1/POC-2; scope question 2 (russh pin
|
||
facts) is verified; questions 1 (the `alk/ssh` topology) and 3
|
||
(feature naming) are decision territory carried as phase 1 OQs —
|
||
the seeded OQ list is in each findings doc.
|
||
|
||
## References
|
||
|
||
- `vision.md` (principles the POCs must honor — including that POC
|
||
code carries relaxed constraints but never merges)
|
||
- `russh.md` (API shapes the POCs exercise), `auth-bridge.md`,
|
||
`mapping-table.md`
|
||
- `docs/sdd_process.md` §Phase 0 (POC modes, standalone placement) |