Renames the written-usage rule into practice across the contract surface: docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files, taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the .opencode/agents/ prompts. Every replacement is a sense-preserving rewrite (colons, semicolons, commas, parentheses, sentence splits, varied), never a mechanical swap; no spaced hyphens were substituted. Enum fields, frontmatter, tables, wire data, and cited identifiers survive verbatim; en dashes inside numeric ranges keep their meaning. Verification: taskgraph validate (43 tasks), repo grep clean (single exempt hit: the verification command's own search pattern in AGENTS.md), cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
133 lines
6.2 KiB
Markdown
133 lines
6.2 KiB
Markdown
---
|
||
status: reviewed
|
||
last_updated: 2026-10-28
|
||
---
|
||
|
||
# Auth: identity is the family's, not SSH's
|
||
|
||
## The model
|
||
|
||
One resolution path exists on every surface: a **verified key's
|
||
fingerprint → alkcall `IdentityProvider::resolve_from_fingerprint` →
|
||
`Identity`**. russh performs all cryptographic work (ADR-002 pin):
|
||
`auth_publickey` fires *after* signature and ownership verification,
|
||
and invalid/malformed signatures produce `USERAUTH_FAILURE` before
|
||
any handler hook runs (POC-1/POC-2 facts). alkssh never sees or
|
||
verifies a signature; its auth surface is *dispositional* (ADR-004).
|
||
|
||
## Server direction
|
||
|
||
### The bridge
|
||
|
||
1. `auth_publickey(user, VerifiedPublicKey)` fires (post-verification;
|
||
the 0.65.0 shape has the key as the `.public_key` field, and the
|
||
client's signature `.algorithm` is exposed for optional hardening).
|
||
2. alkssh computes the fingerprint via the `ssh-key` re-export
|
||
(`HashAlg::Sha256`): `SHA256:` + unpadded base64 over the
|
||
standard alphabet (`ssh-keygen -lf` byte-format; not the
|
||
url-safe alphabet). The string format is a **contract invariant**
|
||
(ADR-004): parity with assembly-registered values is proven
|
||
against independent issuers (`ssh-keygen -lf`; POC-1 §1, pinned
|
||
by the committed vectors in `tests/vectors/`).
|
||
3. `resolve_from_fingerprint` → `Some(identity)` → accept; `None` →
|
||
reject, fail-closed; the resolved-identity-`None` path never
|
||
creates a "weakly authorized" session.
|
||
4. On success the identity rides the session (`auth_succeeded`): for
|
||
mapped channels it propagates through the alkcall transport
|
||
(`set_identity`, once) so `AccessControl::check(peer_identity)`
|
||
gates every mapped open; the **visible-surface invariant** holds
|
||
in the SSH direction with no SSH-shaped exception. POC-3 §C.3
|
||
observed the propagation live (the establisher witness).
|
||
|
||
### Dispositions (ADR-004, summarized)
|
||
|
||
`publickey` is the only implemented method, and the only accepted key
|
||
algorithm is **ed25519**, so `auth_publickey` declines non-ed25519
|
||
public keys explicitly (the family's ed25519-only posture, ADR-002
|
||
decision 4; one key algorithm carries the family's identity: the
|
||
same ed25519 keys behind alktls's self-signed certs, iroh, and the
|
||
alknet rewrite). `auth_publickey_offered`
|
||
accepts-and-delegates (keeping `max_auth_attempts` honest; the hook
|
||
does not consume an attempt, POC-1 verified); `auth_none` /
|
||
`auth_password` / `auth_keyboard_interactive` / `auth_openssh_certificate`
|
||
reject explicitly (at this pin russh's defaults already reject; the
|
||
explicit implementations are the family contract, since a pin bump could
|
||
change defaults silently). `Config::methods` advertises `{publickey}`,
|
||
exactly what is implemented. The username is **not identity and
|
||
carries no semantics** (OQ-03 resolved); log exposure is deployment
|
||
policy. `max_auth_attempts` defaults `10` (the OpenSSH `none`-probe
|
||
consumes one, POC-1 §4).
|
||
|
||
## Client direction
|
||
|
||
### Key custody (ADR-005)
|
||
|
||
Held keys are bearer-token-class secrets: **vault-encrypted at rest
|
||
(alkvault `EncryptedData`, OpenSSH-PEM inner), decrypted at use,
|
||
zeroized ownership at the adapter seam**; never env vars, never
|
||
plaintext at rest, never persisted after use. The vault is a secret
|
||
store on this path, not a key generator, not a custodian, and never
|
||
involved in resolving *peers'* fingerprints. Failure modes are all
|
||
fail-closed with distinct errors: locked / absent / decryption
|
||
failure / unknown `key_version`, and rotation preserves key identity.
|
||
The same model covers **host keys** (decrypt-at-construction into the
|
||
server's `Config::keys`; locked vault → **no server is constructed**;
|
||
POC-2 §5).
|
||
|
||
### Host-key policy (OQ-05 resolution; the client-side trust contract)
|
||
|
||
Two cases, one seam; the crate is **policy-blind**:
|
||
|
||
1. **Known beforehand** (anchors exist; family-built runners mint
|
||
their host key at assembly time): assembly-supplied pins, with a
|
||
**hard decline on mismatch** at `check_server_key`.
|
||
2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no anchor
|
||
until contact): the crate surfaces
|
||
(host, port, presented fingerprint) to an **assembly-provided
|
||
`HostKeyPolicy`**, which chooses 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 and never hard-codes a trust model, so
|
||
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
|
||
|
||
- Password / keyboard-interactive anywhere (server: declined; client:
|
||
non-goal until a use case).
|
||
- OpenSSH certificate auth (`auth_openssh_certificate` declined
|
||
unconditionally; OQ-04 resolved: no CA-trust story exists in the
|
||
family identity model; future support is new-ADR territory with the
|
||
upstream identity-model work as its prerequisite).
|
||
- ssh-agent integration/forwarding (external signer processes are out
|
||
of family scope; the vault is the custody story).
|
||
- TOFU *as crate behavior*, since policy lives at the assembly: the
|
||
crate is policy-blind (above).
|
||
|
||
## Open Questions
|
||
|
||
None open or deferred; see [open-questions.md](open-questions.md)
|
||
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-03 (the
|
||
username disposition, §Dispositions), OQ-04 (the certificate decline,
|
||
§out-of-scope), OQ-05 (the host-key policy seam, §Host-key policy),
|
||
OQ-06 / OQ-07 (custody boundaries, ADR-005).
|
||
|
||
## References
|
||
|
||
- ADR-004 (dispositions + parity invariant), ADR-005 (custody),
|
||
ADR-002 (the pinned russh/auth pipeline facts)
|
||
- `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) 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 |