Files
alkssh/docs/architecture/decisions/004-auth-dispositions.md
T
glm-5.3-flash 263f91fdb7 docs(architecture): review-pass cleanup — inconsistencies, stale refs, scaffolding refresh
- OQ-02: rewritten as a clean resolved statement; contradictory
  deferred-table row removed
- OQ-05: 'old-code anti-pattern' framing corrected — ephemeral-fleet
  accept-and-continue is a legitimate case-2 HostKeyPolicy; the crate
  rule is 'no hard-coded trust model', not 'accept-all forbidden';
  dangling 'expected ADR-007' pointer replaced with the real contract
  locations (auth.md, client.md)
- README: OQ counts corrected (6 resolved / 3 open / 4 deferred);
  sibling references normalized to real ADR IDs (alkcall 010 is alkhttp
  014's analog; 'ADR-016-analog' was a wrong number)
- client/auth: OQ-07 status corrected to resolved-at-adapter-level;
  TOFO typo; TOFU framing
- ADR-004: dangling 'ADR-008' -> serve-config defaults
- ADR-006: SOCKS5 typo
- mapping: alktty typo; '(OQ-… note below)' placeholder -> the real
  POC-3 note reference
- research: cross-repo numbers qualified (alktty OQ-44, alkvault
  ADR-021); reverse-runner lesson 5 reframed per correction
- scaffolding: Cargo.toml description/comments and src/lib.rs doc
  comment updated against ADR-002/003/006 (dynamic-forward retired,
  git exec-only, russh pin decided); CHANGELOG logs POC completion and
  ADRs 001-006

Verification: cargo test, cargo clippy --all-targets -- -D warnings,
cargo fmt --check, cargo check — all pass
2026-10-27 10:00:11 +00:00

4.5 KiB
Raw Blame History

status, last_updated
status last_updated
draft 2026-10-27

ADR-004: Auth dispositions — publickey-only, fail-closed, fingerprint identity

Status

Accepted — 2026-10-26. Evidence-backed (POC-1 end to end, POC-2's signature-pipeline facts); open to amendment only via a new ADR. Not a ledger item (no wire format changes), but the decline matrix is contract territory: every decline is tested on both sides of every feature.

Context

The family identity model has exactly one SSH-shaped resolution path: a verified key's fingerprint → alkcall IdentityProvider:: resolve_from_fingerprint → Identity (auth-bridge.md). russh 0.64.1 performs the cryptographic verification itself — auth_publickey fires after signature and ownership verification (POC-1 observed), and invalid/malformed signatures produce USERAUTH_FAILURE before any handler hook fires (POC-2). alkssh therefore never touches signature material; its auth surface is dispositional.

The fail-closed posture (family ADR-003 decision 3) applies to auth twice: which methods exist, and what unauthenticated attempts receive.

Decision

  1. auth_publickey is implemented — the only method. Handler: fingerprint (ssh-key re-export, HashAlg::Sha256, SHA256: + unpadded url-safe base64) → resolve_from_fingerprint → Some → accept with the resolved identity; None → reject. The fingerprint string format is a contract invariant: registration strings from any standard issuer (ssh-keygen, alkcall-side computation) must compare equal — POC-1 proved parity against ssh-keygen -lf output.
  2. auth_publickey_offered is accepted-and-delegated. The probe hook (key seen, ownership unverified) accepts the offer and lets auth_publickey decide — which keeps max_auth_attempts counting honest (confirmed: the hook does not consume an attempt).
  3. Every other method is explicitly declined, including when inert at the pin. auth_none, auth_password, auth_keyboard_interactive, auth_openssh_certificate each get explicit reject implementations. At russh 0.64.1 the defaults already reject (POC-1's correction of the 0.5x-era assumption) — the explicit implementations are family contract, not redundancy: the pin is a one-way door and a future bump could silently change defaults (an auth-posture change would otherwise be invisible). The certificate decline is the standing disposition (OQ-04 tracks whether a CA-trust use case ever arrives).
  4. Honest advertisement. Config::methods advertises exactly what is implemented: {publickey} in every current configuration. OpenSSH honors the subset (POC-1: non-advertised methods are never attempted), so the advertisement itself is part of the fail-closed contract.
  5. The username is not identity and carries no semantics. Keys resolve independently of the username string (POC-1: ssh anything@host authenticates identically). Whether a username appears in logs is deployment policy, outside the crate (OQ-03).
  6. Bounded auth resources. max_auth_attempts is assembly-configurable with a default sized for OpenSSH's counting none-probe: POC-1 measured that the probe consumes one attempt, so a default of 10 leaves IdentitiesOnly-style clients at least nine real key attempts; a small limit eats one attempt on every connection (recorded for the serve-config defaults). Constant-time rejection is inherited from Config::auth_rejection_time (russh-managed).

Consequences

  • Positive: the entire auth surface is evidence-backed end to end (vanilla clients, both directions of failure); the decline matrix is small and fully testable per build; auth posture survives a russh bump that changes defaults.
  • Negative: none material. The explicit-declines cost a few handler lines per method — deliberately purchased immovability.
  • Neutral: password/keyboard-interactive exist client-side only as non-goals (vision.md) until a consuming use case arrives; the server never gains them without amending this ADR (a wire-visible capability, not a two-way door).

References

  • auth-bridge.md (the mapping + disposition table + 0.64.1 default note)
  • docs/research/poc-1-auth-bridge.md §2–4, §OQ seeds 1–4
  • docs/research/poc-2-vault-keys.md §"Server-side auth pipeline" (invalid signatures pre-hook)
  • Family ADR-003 (decisions 2, 3), alkcall ADR-002/003/006 (the IdentityProvider surface and its single-backend posture; alkhttp ADR-073's one-backend-per-door analog)
  • OQ-03 (resolved), OQ-04 (deferred)