Expand description
Account and credential self-service (D36).
Adding a user used to mean the operator appending a line to the
--auth-file by hand and restarting the daemon, then appending
another to the --acl-file, then a third to an authorized_keys.
This module is the same three facts issued through the node instead:
one record per account holding the BLAKE3 of its token, the grants it
was issued with, and its registered SSH keys.
Three properties carry the design.
Issuance is invite-only. There is no registration endpoint. An
account exists because a holder of @node write minted a single-use,
expiring invite naming it, and the invited person redeemed that
invite. The invite is a credential: it is presented as ordinary
basic auth, so the node’s 401 wall, its constant-time compare and
D33’s per-user buckets all apply to redemption without a line of new
code, and an anonymous request still reaches nothing. An unauthenticated
redeem route would have been the one unmetered, unattributed way in.
Issued grants are enforced by the D29 table, not beside it. The
store renders its grants as ACL lines and hands them to
Acl::parse, and Accounts::acl returns the result. The node
merges that with the file table, so the git chokepoint,
crate::acl::api_denial, crate::acl::filter_response, D33’s
@node exemption and the choir-ssh shim all grade an issued
credential without any of them knowing this module exists. The store
can never grant crate::acl::Scope::Node: node-wide authority stays operator-
authored in the ACL file, so self-service cannot mint itself an
auditor or a rate-limit exemption.
This is not in the op log, and that is the decision. D35 measured
what a new OpKind variant costs: the fold has no tolerant arm, so
every reader older than the variant stops materializing the log at the
first op it does not know, totally rather than partially. Paying that
for credentials would also buy the wrong property — the log is
append-only and replayed in full by any @node auditor, and a token
hash placed there could never be forgotten, while revocation’s whole
contract is that it forgets. So the store is a node-owned file,
rewritten in place, and revocation is deletion.
Structs§
- Accounts
- The credential store: accounts, invites, and the two files it owns.
- Invite
Summary - What an invite promises its holder, read by the join page (D57).
- Request
Summary - One pending request, as the operator’s queue and the asker’s own waiting page read it (D72).
- SshKeys
Out - Where a generated
authorized_keysgoes and what the forced command in it should say (D31’s file, written by the node instead of by hand).
Enums§
- Principal
- Who a set of presented credentials turned out to be.
Constants§
- DEFAULT_
INVITE_ SECS - How long an unredeemed invite stays usable when the caller names no lifetime: one day, which is longer than handing someone a secret takes and shorter than forgetting about it does.
- FORMAT_
VERSION - Version of the on-disk store. Every persisted struct carries one (invariant 1); new fields are additive and old files still load.
- GRANTED_
REQUEST_ SECS - How long the invite a granted request becomes is good for (D72).
- INVITE_
PREFIX - Prefix every invite id carries, and a spelling
validate_usernamerefuses, so an invite can never name the same principal an account does. - MAX_
ABOUT_ CHARS - Longest
aboutline a request may carry (D72). One line, not a letter: the operator reads a queue of these, and the decision they are making is whether to let somebody in, not whether the essay was good. - MAX_
CREDENTIAL_ ID_ CHARS - Longest credential id accepted. The spec allows up to 1023 raw bytes; this is that ceiling in base64url, so nothing legitimate is refused and an unbounded string is.
- MAX_
INVITE_ SECS - Longest lifetime an invite may be given. An invite is a bearer secret for an account that does not exist yet, so “expires eventually” is not the same promise as “expires”.
- MAX_
LABEL_ CHARS - Longest passkey label. Long enough to say “work laptop, touch id”, short enough that a roster stays a roster.
- MAX_
PASSKEYS - Most WebAuthn credentials one account may enrol (D39). A person has a laptop, a phone and a hardware key; a hundred is not a person with many devices, it is a store being filled by something automated.
- MAX_
PENDING_ REQUESTS - The most unanswered requests this node holds at once (D72).
- REQUEST_
PREFIX - Prefix on an access-request id (D72), so an id says which table it
belongs to without a lookup. Disjoint from
INVITE_PREFIXbecause the two live in different maps and a reader comparing a claim link against a log line should not have to guess which one they are holding. - REQUEST_
TTL_ SECS - How long an unanswered access request survives (D72).
Functions§
- grants_
acl - The grants in a store, as an
Aclfile body. - validate_
actor_ key - Checks an ed25519 actor public key and returns it lowercased.
- validate_
display_ name - Checks a readable display name (D46).
- validate_
grant - Checks one grant column pair against the real ACL parser and returns it in canonical spelling.
- validate_
ssh_ key - Checks an OpenSSH public key line and returns it without its comment.
- validate_
username - Checks a name the node will interpolate into an
authorized_keysforced command and key on for every authorization decision.