Files
alkssh/docs/architecture/auth.md
T
glm-5.3-flash 22b82af517 docs(architecture): correct the fingerprint base64 alphabet wording
The format contract (ADR-004 decision 1) is SHA256: + unpadded
standard base64 — not url-safe. The ssh-key re-export and ssh-keygen
both emit the +/ alphabet (visible in the recorded fingerprints); the
'url-safe' wording in ADR-004/auth.md/plans/implementation.md and the
POC-1 §1 finding was a misnomer. Wording corrected; the parity
contract itself unchanged on both sides.
2026-10-28 06:27:49 +00:00

133 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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;
0.65.0 shape — the key is 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, and 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** — `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 — 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 —
**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`** — 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 —
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* — 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