michal/tit
Browse tree · Show commit · Download archive
Blob: docs/adr/0011-web-login-sessions.md
# Architectural decision record 0011: Web login sessions Status: Accepted Date: 2026-07-22 Amended: 2026-07-24 ## Context The Web UI must use the same SSH key identity as the SSH server. A login challenge must not be reusable. A database copy must not contain a session token or a CSRF token that a user can use directly. ## Decision The primary login form creates a five-minute SSH approval. The Web UI shows an exact `ssh` command with a random one-time secret. The browser stores a different login CSRF value in an `HttpOnly` cookie. SQLite stores only the SHA-256 hashes of these two values. The user runs the command against the built-in SSH server. Public-key authentication selects the account and active key. The `login` command binds the pending approval to that account and key in one transaction. Its output shows the Web origin and account. The browser supplies the one-time secret and the matching login CSRF value when it completes the login. In one SQLite transaction, the server consumes the approval and creates a seven-day session. The fallback form requires only a username. The server creates a five-minute `tit-auth-v2` challenge and stores only the SHA-256 hash of its random nonce. The user downloads and signs the exact challenge with the `tit-auth` SSHSIG namespace. The SSHSIG envelope contains the signing public key. The server verifies that this key is active on the named account. The user can upload the signature file or paste the complete envelope. An HTML form changes challenge line endings to CRLF. The HTTP interface changes these line endings back to LF before signature verification. A failed signature returns the same challenge page so that the user can try again before it expires. The response stores an opaque session token in an `HttpOnly` cookie and a CSRF token in a second cookie. Both cookies use `SameSite=Strict`. HTTPS responses also use `Secure`. SQLite stores only SHA-256 hashes of both values. Challenge creation also sets a five-minute, `HttpOnly` login CSRF cookie. The verification form must submit the matching value, and the nonce row stores only its hash. Thus, a different site cannot complete a login in the user's browser. Each state-changing account form must compare its submitted CSRF token with the CSRF cookie and the stored hash. The initial account page supplies a logout form that uses this rule. Logout ends all sessions for the account. Recovery, key addition, key revocation, suspension, and restoration also end all account sessions in the same transaction as the privilege change. ## Failure and threat cases Login and signature forms have fixed body limits. Axum limits the request body, and `multra` parses the uploaded form as a stream within the 64-KiB limit. Challenge and SSHSIG parsing have the authentication limits from architectural decision record 0002. A bad identity, signature, expired challenge, and consumed challenge use a common Web UI error. The response does not disclose which input failed. The server keeps a maximum of 1,024 active login nonces and 1,024 active SSH approvals. It removes consumed or expired records before it creates one. Cookie parsing rejects duplicate, empty, and oversized values. A session is not valid after its expiry, account suspension, logout, or a privilege change. A CSRF failure does not end the session because an external request must not be able to log out a user. ## Evidence The session test signs with stock `ssh-keygen`, restarts the login service between issue and verification, rejects challenge replay and a bad CSRF token, checks the stored hashes, invalidates a session after key addition, and tests the function that ends all sessions. It also verifies approval binding and concurrent one-time consumption. The executable test creates an approval through HTTP, approves it with stock `ssh`, and completes the browser session. It also completes the fallback with stock `ssh-keygen` and browser CRLF line endings. It rejects an incorrect upload content type and malformed multipart content. It confirms CSRF rejection and session invalidation. ## Consequences The primary flow needs access to the built-in SSH service. The fallback works when the user cannot connect to that service. Neither flow asks the user to copy a public key.