Files
alkssh/docs/architecture/auth.md
T
glm-5.3-flash 1bfd07adfc docs(architecture): the four spec docs — overview, mapping, client, auth
- overview.md: two-doors framing, family-position table, surface
  categories, bounded-resources policy (max_auth_attempts 10,
  keepalives 15s/3, per-channel caps), non-goals, lifecycle note
- mapping.md: the spine — channel/request rows (git exec-only, route-1
  bridge, translation-bridge session rows), parse-only dispatch,
  global-request rows, spawn-and-return constraint, per-build decline
  matrix
- client.md: dial surface (key source + host-key policy + keepalives),
  op rows (family types in, vanilla out), fail-closed acceptor
  (not russh's accept-and-drop), lifecycle primitives only, POC
  correctness facts carried
- auth.md: the fingerprint bridge + dispositions summary (server),
  vault custody + two-case host-key policy (client)
- README index: doc table filled, current state updated
2026-10-27 03:07:17 +00:00

5.3 KiB
Raw Blame History

status, last_updated
status last_updated
draft 2026-10-27

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 url-safe base64. 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).
  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; 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, or learn-and-record. The OpenSSH "add to known_hosts?" moment becomes assembly policy.

Crate rules: default policy is fail-closed (reject unknown); the crate never decides trust; accept-all is never a crate option (alkminer's accept-all is the recorded anti-pattern; for ephemeral fleets the real anchor is hub-held key material — per ADR-005's custody model — 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 declines; OQ-04 deferred on a CA use case).
  • ssh-agent integration/forwarding (external signer processes are out of family scope; the vault is the custody story).
  • TOFO/TOFU as crate behavior — policy lives at the assembly, the crate is policy-blind (above).

Open Questions

  • OQ-04 (deferred — certificate auth, on a use case).
  • OQ-07 (open — the vault decrypt plain-String gap; upstream ask to alkvault; adapter-side Zeroizing convention stands meanwhile; ADR-005 decision 3 is the contract).
  • OQ-05 (resolved — recorded above).

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), alkhttp ADR-014/073 analogs (secret flow, one-backend posture), family ADR-002 decision 5 / family ADR-003 decision 2