diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-03-07 11:45:19 -0500 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-03-07 11:45:19 -0500 |
| commit | 7c32f8a87889c8fdb8637243fd540061ea1a8539 (patch) | |
| tree | fe5b5b42f3cfe74e755cf57419b7f3d3b6dd89bb | |
| parent | 36381d86c6a690a7870ce360dad63be333361447 (diff) | |
| download | buth-7c32f8a87889c8fdb8637243fd540061ea1a8539.zip | |
doc: simplify architecture, some notes
Got rid of client-side 9p, which was just silly bloat. Using HTTP
exclusively on the client side now, with htmx.
Combined auth and api servers into monolithic buthd, which translates
http/9p between client and backend 9p servers.
Added some implementation and security notes, and part of shopfs.
| -rw-r--r-- | doc/api.md | 19 | ||||
| -rw-r--r-- | doc/arch.dot | 19 | ||||
| -rw-r--r-- | doc/arch.md | 33 | ||||
| -rw-r--r-- | doc/arch.png | bin | 41371 -> 24198 bytes | |||
| -rw-r--r-- | doc/auth.md | 57 | ||||
| -rw-r--r-- | doc/authfs.md | 4 | ||||
| -rw-r--r-- | doc/sec.md | 13 | ||||
| -rw-r--r-- | doc/shopfs.md | 17 |
8 files changed, 107 insertions, 55 deletions
diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..8436144 --- /dev/null +++ b/doc/api.md @@ -0,0 +1,19 @@ + +# API usage + +This document describes how a client uses buthd's API once he's [logged in](auth) (has a session cookie). + +An example: adding an item to the shopping cart. +- Client `GET`s `shop./foo.html` + - Includes session cookie in request +- httpd returns static page and scripts +- Client interacts with page, e.g. presses "add to cart" +- htmx `POST`s a form to `api./cart` containing `<sku>` and `<quantity>` + - Includes the session cookie in the request +- `buthd` receives the request, extracts the session ID from the session cookie +- `buthd` uses the session ID to look up the user's username by reading `/sessions/<id>/user` from [[authfs]] +- `buthd` adds `<quantity>` units of `<sku>` to `<username>`'s cart using [[shopfs]] + +Other parts of the API behave similarly: client posts to buthd, buthd uses 9P servers to complete the request. + +TODO: define and document all of buthd's HTTP endpoints diff --git a/doc/arch.dot b/doc/arch.dot index fa6f5aa..9bf558d 100644 --- a/doc/arch.dot +++ b/doc/arch.dot @@ -1,18 +1,15 @@ graph { - graph [rankdir=BT; nodesep=1.0; ranksep=1.0] + graph [rankdir=BT; nodesep=0.75; ranksep=0.5] node [shape=box] - client -- relay [label="HTTPS"] - client -- relay [label="9P/WS"] - relay -- shop [label="HTTP\nstatic html"] - relay --api [label="HTTP\nhtmx frags"] - relay -- api [label="9P/WS\napi"] - relay --auth [label="HTTP\nforms"] - api -- authfs [label="9P/TCP"] - auth --authfs [label="9P/TCP"] + client -- relayd [label="HTTPS"] + relayd -- httpd [label="HTTP\nstatic html"] + relayd --buthd [label="HTTP\nhtmx frags\napi"] + buthd -- authfs [label="9P"] + buthd -- shopfs [label="9P"] subgraph cluster_lan { - relay, shop, api, auth - authfs [shape=cylinder] + relayd, httpd, buthd, authfs, shopfs + {node [shape=cylinder]; authfs, shopfs} } } diff --git a/doc/arch.md b/doc/arch.md index faac44e..4a5a0e1 100644 --- a/doc/arch.md +++ b/doc/arch.md @@ -1,7 +1,7 @@ # Architecture -Intended to be deployed on OpenBSD. +Intended to be deployed on OpenBSD with all processes running with minimal privileges. - LAN - shop.samanthony.xyz @@ -9,27 +9,36 @@ Intended to be deployed on OpenBSD. - Serves static HTML files - Serves scripts (js/wasm) including htmx.js - api.shop.samanthony.xyz - - `buthapi` API server + - `buthd` API server - Serves htmx fragments - - Serves 9P {/cart, /checkout} to authenticated clients via websockets - - auth.shop.samanthony.xyz - - `buthauth` web authentication gateway - - Client-facing HTTP interface to authfs - - Handles registration and login forms + - Proxy between client HTTP and backend 9P servers + - Handles login forms: client↔authfs + - Handles cart and checkout endpoints: client↔shopfs - authfs - - `buthauthfs` daemon + - `buthauthfs` 9P file server daemon - Persistent user database - Stores password hashes - Manages client sessions - - Serves 9P to api and auth servers - - relay - - relayd(8) + - shopfs + - `buthshopfs` 9P file server daemon + - Inventory + - User shopping carts + - Checkout + - relayd(8) - TLS proxy/gateway - WAN - Client web browser - HTML renderer, js/wasm interpreter - - Generates and stores its session ID (in a cookie) + - Generates and stores session cookies The LAN could be either a single OpenBSD host, several vmd(8) VMs, or several machines in a VPN, e.g. Tailscale. +An administrator can manage the site by mounting the `fs` daemons in a terminal's namespace with userspace plan9 utils. + ![[arch.png]] + +## Notes + +Should `buthd` be monolithic, or should there be one http/9p gateway per 9p file server, multiplexed by relayd? should the htmx fragment serving/template rendering be broken out into its own process? + +Nice to minimize number of http servers (just httpd and buthd). Less sysadmin: relayd, permissions, init scripts. diff --git a/doc/arch.png b/doc/arch.png Binary files differindex d9cb9f0..50fa34f 100644 --- a/doc/arch.png +++ b/doc/arch.png diff --git a/doc/auth.md b/doc/auth.md index a8aeb25..e509e44 100644 --- a/doc/auth.md +++ b/doc/auth.md @@ -1,50 +1,47 @@ # Authentication -## Registration flow +## Registration This is how a new user registers himself in `authfs`, after which he can open sessions. - Client `GET`s `shop./register.html` -- Client enters username and password, `POST`s form to `auth./register` -- `buthauth` server registers user in `authfs` +- Client enters username and password, `POST`s form to `api./register` +- `buthd` registers user in `authfs` - Creates `/users/<username>/` - If it already exists, the username is taken; return error to client - Writes password to `/users/<username>/passwd` - `authfs` ingests and hashes the password. Subsequent reads of `/users/<username>/passwdhash` will return the hash (`authfs` discards the cleartext password after it is hashed). - If successful, client can now login to obtain a session -## Login flow +## Login This is how a client authenticates itself and creates a _session_. A session gives the client a shopping cart and access to the checkout. - Client `GET`s `shop./login.html`. -- Client enters username and password (assume they already have an account). -- Js or wasm script running in client generates a 256-bit session ID and injects it into the login form. -- Client `POST`s the login form to `auth./login` -- `buthauth` writes the password to `/users/<username>/login` in `authfs` +- Client enters username and password. +- JS script running in client generates a 256-bit session ID and injects it into the login form. +- Client `POST`s the login form to `api./login` +- `buthd` writes the password to `/users/<username>/login` in `authfs` - If `/users/<username>/` doesn't exist, the username is invalid; fail the login -- `buthauth` reads from `/users/<username>/login`. +- `buthd` reads from `/users/<username>/login` - If the password was correct, `authfs` returns a session ID corresponding to `/sessions/<id>/ - - Otherwise if the password was wrong, `authfs` returns `Rerror`. `buthauth` fails the login, returning an error to the client -- `buthauth` returns a session cookie, containing the session ID, to the client (with proper security params: HttpOnly etc.) + - Otherwise if the password was wrong, `authfs` returns `Rerror`. `buthd` fails the login, returning an error to the client +- `buthd` returns a session cookie containing the session ID to the client (with proper security params: HttpOnly etc.) - Client includes the session cookie in subsequent requests, giving it elevated privileges. On the client, session is a _session cookie_ (no expiry date). On the server, it should expire after `lastseen+t`. `authfs` periodically sweeps the sessions and removes old ones. `authfs` updates a sessions's last-seen time whenever it gets a request on `/sessions/<id>/*`. -## API usage -This is how a client accesses the 9P API once he's logged in (has a session cookie). -- Client `GET`s `shop./foo.html` - - Includes cookie in request -- Httpd returns static page and scripts -- Script opens websocket on `api./ws` -- Browser sends cookie automatically in upgrade request -- `buthapi` validates session (using cookie) by checking if `/sessions/<id>/` exists on `authfs`. - - If not, the session is invalid, return 401 -- `buthapi` upgrades the websocket connection and serves 9P -- Client uses ws/9p connection to access API - - E.g. pressing "add to cart" button does `Tcreate /cart/sku123` - - (will need to figure out how to wire this up with htmx/js and 9p js/wasm lib) - -## Logout flow +Generate the session id on the client side to take advantage of the abundance of entropy there compared to on the server. + +## Logout This is how a client destroys an active session on the server. - Client clicks Logout button -- Htmx `POST`s to `auth./logout`, includes the session cookie in the request -- `buthauth` reads the session ID from the cookie -- `buthauth` removes `/sessions/<id>` on `authfs` -- `buthauth` returns a response header telling the client browser to destroy the session cookie. +- htmx `POST`s to `api./logout`, includes the session cookie in the request +- `buthd reads the session ID from the cookie +- `buthd` removes `/sessions/<id>` on `authfs` +- `buthd` returns a response header telling the client browser to destroy the session cookie. + +## TODO +- Allow the user to register an email for password recovery? +- Document changing password: buthd writes to `/users/<username>/passwd` + +## Implementation notes + +Don't leave 9p connections laying around. Open a new one for each http request as required. This is to avoid attacks like the following. +Suppose buthd has a single connection to authfs that it uses for every http connection. Alice (the attacker) and Bob are both trying to login as `bob`. Alice's connection writes a junk password to `/users/bob/login`. Bob's connection writes the correct password. Alice's connection reads `login`, obtaining the session ID, as does Bob's connection. Now Alice has Bob's session ID and can act as Bob. This is bad. diff --git a/doc/authfs.md b/doc/authfs.md index 0f3ee6f..c3d0488 100644 --- a/doc/authfs.md +++ b/doc/authfs.md @@ -10,9 +10,9 @@ `user` `/users/<username>/` (d) -To add a user, create the `<username>` subdirectory. Authfs +To add a user, create the `<username>/` subdirectory. Authfs automatically creates the `passwd`, `passwd`, and `login` files -inside. +inside. Initially the password is nil and login is disabled. `/users/<username>/passwd` (w) Writing a (cleartext) password changes the user's password. diff --git a/doc/sec.md b/doc/sec.md new file mode 100644 index 0000000..93bca91 --- /dev/null +++ b/doc/sec.md @@ -0,0 +1,13 @@ + +# Security + +## Notes + +Buthd should not have filesystem permission. It just translates HTTP/9P. Gets everything it needs from the 9P file servers. + +Concentrate TLS in relayd(8). Backend servers should not have to manage certificates, or even have access to them. They should not even have filesystem permission if possible. Buthd and httpd serve plain HTTP over Unix domain sockets or a secure VPN, e.g. Tailscale. + +Run all processes in chroot. This should be handled by the rc.d init script, not by the program, to avoid having to start as root before dropping privileges. Just start as unprivileged user in chroot to begin with. + +[[auth#Implementation notes]] + diff --git a/doc/shopfs.md b/doc/shopfs.md new file mode 100644 index 0000000..6e1e199 --- /dev/null +++ b/doc/shopfs.md @@ -0,0 +1,17 @@ + +# 9P files served by the `shopfs` daemon + +`carts/` + `<username>/` + `<sku>` + +`/carts/<username>/<sku>` (rw) +Creating the file `<sku>` adds a single item of `<sku>` (stock keeping +unit) to user `<username>`'s cart. The initial quantity is 1 unit. +Read returns the number of items of that type in the user's cart, in +text form (`[1-9][0-9]*`). Writing a number to the file adds or +removes the specified number of items to/from the cart +(`[+-]?[1-9][0-9]*`). Removing the file removes (all units of) the +item from the cart. + +TODO: checkout |