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

6.3 KiB
Raw Blame History

status, last_updated
status last_updated
reviewed 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 (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