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.
5.1 KiB
status, last_updated
| status | last_updated |
|---|---|
| reviewed | 2026-10-28 |
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. Amended 2026-10-27 (review pass, per ADR-002 decision 4's
ed25519-only amendment): auth_publickey additionally declines
non-ed25519 keys explicitly (the key-algorithm posture, not the
fingerprint disposition, changes); all other dispositions stand. 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
(the POC pin) performs the cryptographic verification itself, so
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); the facts
carry to the 0.65.0 pin per ADR-002's upgrade note, where the handler
field shape auth.md cites is normative. 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
auth_publickeyis implemented, and it is the only method. Handler: fingerprint (ssh-keyre-export,HashAlg::Sha256,SHA256:+ unpadded base64 over the standard alphabet,+/, not the url-safe alphabet; byte-identical tossh-keygen -lf) →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 againstssh-keygen -lfoutput.auth_publickey_offeredis accepted-and-delegated. The probe hook (key seen, ownership unverified) accepts the offer and letsauth_publickeydecide, which keepsmax_auth_attemptscounting honest (confirmed: the hook does not consume an attempt).- Every other method is explicitly declined, including when inert
at the pin.
auth_none,auth_password,auth_keyboard_interactive,auth_openssh_certificateeach 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 resolved: no CA-trust use case arrives in the family identity model; future support is new-ADR territory with the upstream identity-model work as its prerequisite). - Honest advertisement.
Config::methodsadvertises 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. - The username is not identity and carries no semantics. Keys
resolve independently of the username string (POC-1:
ssh anything@hostauthenticates identically). Whether a username appears in logs is deployment policy, outside the crate (OQ-03). - Bounded auth resources.
max_auth_attemptsis assembly-configurable with a default sized for OpenSSH's countingnone-probe: POC-1 measured that the probe consumes one attempt, so a default of10leavesIdentitiesOnly-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 fromConfig::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–4docs/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
IdentityProvidersurface and its single-backend posture; alkhttp ADR-073's one-backend-per-door analog) - OQ-03 (resolved), OQ-04 (resolved)