Skip to main content

max / makenotwork

9.7 KB · 198 lines History Blame Raw
1 # Deploying multithreaded
2
3 Two instances, two mechanisms, because they are different architectures.
4
5 | Instance | Host | Arch | Mechanism |
6 |----------|------|------|-----------|
7 | staging demo | testnot-1 | x86_64 | Sando, as a companion of the MNW server |
8 | production | alpha-west-1 | x86_64 | Sando, as a companion of the MNW server |
9 | astra | astra | aarch64 | `deploy/deploy.sh` |
10
11 testnot-1 carries a **read-only demo**, provisioned 2026-08-06. It is seeded from
12 mt's own `--seed`, holds no prod data, and is read-only by construction rather
13 than by a flag: every write path needs a session, a session needs a completed
14 OAuth login, and no OAuth client is registered there. Unauthenticated POSTs come
15 back 403.
16
17 That is what makes mt browser-testable at all — the read half of it. Before this,
18 every audit run skipped its browser axis, so mt template and frontend work shipped
19 without the verification every other project gets. Write coverage still needs the
20 astra harness instance below.
21
22 Sando builds only on its configured `build_host` (fw13, x86_64) and refuses to
23 compile anywhere else, so it cannot produce the aarch64 binary astra needs. That
24 is the same constraint that sent pom to Bento. Astra keeps its own script.
25
26 ## Production: a Sando companion
27
28 There is no multithreaded-specific deploy command. mt is built from the same
29 worktree and sha as the MNW server, staged into the same release bundle, and
30 installed on `testnot-1` and `prod-1` after the server's symlink swap. Run a
31 normal MNW deploy and mt goes with it.
32
33 What that buys, none of which the old script had: a native build on fw13, the
34 `migration_dry_run` gate against mt's own prod dump, the `cargo_test` gate, the
35 frontend build treated as fatal rather than as a `cargo::warning`, a 48h burn-in
36 on testnot, `node_health`, and a binary that ships from the same sha as the
37 server it talks to.
38
39 Config lives in two files in `sando/`, split the way Sando splits everything:
40 `daemon/sando-daemon.toml` (and `deploy/sando-daemon.toml.example`) says mt is a
41 `[[companion]]` to build, and `sando.toml` says which nodes install it.
42
43 The cost is that `Cargo.toml`'s version goes decorative, as mnw-cli's already
44 has. mt is not distributed on its own, so an independent version buys nothing.
45 Releases are identified by the bundle digest and the sha.
46
47 ### Why the binary has no `static/` beside it
48
49 mt embeds its `static/` tree (`src/static_assets.rs`) instead of serving it from
50 a directory next to the binary. A companion is exactly one installed file, and
51 `build.rs` bakes a content hash of those assets into the templates compiled into
52 the binary — so a binary-only install would serve new markup against old CSS
53 with nothing reporting an error. Embedding makes the two inseparable.
54
55 Consequence for anyone editing assets: a change to `static/style.css`, `mt.js`
56 or the TypeScript under `frontend/src` needs a rebuild to take effect. It
57 already did, since those are what the `?v=` cache-buster hashes.
58
59 ### One-time node setup
60
61 None outstanding. prod-1 has carried `/usr/local/lib/mnw/install-companion.sh`
62 and the `/etc/sudoers.d/mnw-companion` grant since mnw-cli, and the grant is on
63 the script rather than on a particular companion. testnot-1 got both on
64 2026-08-06, along with the postgres role and database, the system user,
65 `/opt/multithreaded/.env`, and the unit.
66
67 One testnot-only wrinkle worth recording, because it cost a debugging round and
68 will bite the next node set up the same way. testnot's `pg_hba.conf` has
69 `local all all peer map=mnw`, where prod has a plain `local all all peer`. A map
70 does **not** imply the identity pairing, so an OS user connecting as its own
71 same-named role is refused (`28000 Peer authentication failed`) until it is
72 listed in `pg_ident.conf`. `postgres:///multithreaded` therefore needed:
73
74 ```
75 mnw multithreaded multithreaded
76 ```
77
78 Any future node hosting mt needs both files, as root, from `sando/deploy/`:
79
80 ```
81 sudo install -d /usr/local/lib/mnw
82 sudo install -m 0755 install-companion.sh /usr/local/lib/mnw/install-companion.sh
83 sudo install -m 0440 mnw-companion.sudoers /etc/sudoers.d/mnw-companion
84 sudo visudo -cf /etc/sudoers.d/mnw-companion
85 ```
86
87 The sudoers file names every deploy user Sando SSHes as — `makenotwork` for
88 prod-1, `deploy` for testnot-1 — so it installs verbatim and needs no hand-edit.
89 A node Sando reaches as some other user needs that user added to the file, or the
90 install is denied, and denied late: companions install after the symlink swap.
91 Check the grant by hand before relying on it:
92
93 ```
94 ssh <deploy-user>@<node> sudo /usr/local/lib/mnw/install-companion.sh
95 ```
96
97 Exit 2 with the usage line means the grant works. A password prompt or a "not
98 allowed" means it does not.
99
100 mt also needs what it has always needed on the box, none of which Sando touches:
101 the `multithreaded` postgres role and database, `/opt/multithreaded/.env`,
102 `multithreaded.service` installed and enabled (`deploy/multithreaded.service`),
103 and a working OS trust store (see below).
104 Sando installs the binary and restarts the unit; it does not create users,
105 databases, env files, or units, and it will not rewrite a config that drifted.
106
107 ### The OS trust store is a precondition
108
109 mt ships no trust anchors of its own. Every outbound TLS connection it makes
110 reads the host CA bundle, which on a Debian-family box means `/etc/ssl/certs`
111 kept current by the `ca-certificates` package. Three paths depend on it: the
112 OAuth token exchange with the MNW server (`src/auth.rs`), link previews
113 (`src/link_preview.rs`), and S3 or MinIO media through `s3-storage`. `reqwest`
114 reads the store through `rustls-platform-verifier` and the AWS client through
115 `rustls-native-certs`; neither offers bundled roots as an option any more, so
116 there is no in-binary fallback to fall back to.
117
118 A host with a thin or stale bundle does not fail in a way that looks like a
119 deploy problem. mt starts, `/api/health` passes because it only probes the
120 database, and pages serve. The first outbound TLS request is a login attempt,
121 because mt is an OAuth relying party, so the symptom is a login outage with a
122 certificate error buried in the journal on a node that reports healthy.
123
124 Two things now make that visible. mt probes the store at startup and logs an
125 error when it comes back empty, and `/api/health` carries a `tls_trust_anchors`
126 field that PoM asserts, so the condition surfaces as a degraded target rather
127 than as a support ticket. Neither watches the host bundle for staleness over
128 time; that is infra's job, and keeping `ca-certificates` on an update path is
129 still a precondition rather than something mt arranges.
130
131 Rollback is the MNW server's rollback: the previous bundle still holds the
132 previous mt binary.
133
134 ## astra: the write-enabled harness instance
135
136 `deploy/deploy.sh` installs mt on astra. That instance is not a second staging
137 box in general; it exists so an audit run's browser lens can **write**.
138
139 The public demo (testnot) is deliberately read-only and no-login, which leaves
140 every mutation path browser-unverified, and mutation paths are where
141 rendered-behaviour bugs live: the CSRF token round-tripping through a real form
142 submission, HX swap targets, optimistic UI, the compiled islands once
143 `static/dist` is loaded. Verifying those needs a session, and a session needs a
144 real OAuth login. So the harness instance is reachable over the tailnet only,
145 logs in against testnot's MNW, and is wiped between runs.
146
147 Reached at `https://astra.tailc6b3e1.ts.net:3443`, published by `tailscale serve`
148 (`tailscale serve --bg --https=3443 http://127.0.0.1:3400`) and reachable from
149 the tailnet only. mt itself binds `127.0.0.1`, so the app port is on no other
150 interface and the tailnet proxy is the only way in.
151
152 TLS is not optional here, and finding that out is worth two minutes of someone
153 else's time: `config.rs` refuses to boot a non-loopback deployment on plain
154 `http`, because that would carry OAuth codes and session cookies in the clear.
155 `tailscale serve` is what supplies the certificate — astra's own Caddy already
156 holds `:443` for something else, hence the `:3443`. Set `COOKIE_SECURE=true`
157 to match.
158
159 ### What it depends on, on the MNW side
160
161 Login is the whole point, so mt is only half the setup. The other half is
162 testnot's example seed, whose harness phase creates the accounts and the OAuth
163 client (`server/src/seed/harness.rs`):
164
165 | Piece | Value |
166 |-------|-------|
167 | `OAUTH_CLIENT_ID` | `mt-astra-harness` (a `sync_apps` row; PKCE, no secret) |
168 | Registered redirect | `https://astra.tailc6b3e1.ts.net:3443/auth/callback` |
169 | Accounts | `harness_fan` (Fan+), `harness_creator` (creator), `harness_owner` |
170 | Password | shared, from `MT_HARNESS_PASSWORD` on the testnot box |
171
172 That phase runs only when `MT_HARNESS_PASSWORD` and `MT_HARNESS_REDIRECT_URI`
173 are both set in testnot's `EnvironmentFile` (`/etc/mnw/makenotwork.env`), which
174 `mnw-testnot-seed.sh` already passes through. Neither value is in the repo: one
175 is a credential, the other names a tailnet host.
176
177 The three accounts carry fixed MNW ids, and mt's own seed (`src/seed.rs`)
178 assigns their community roles by `mnw_account_id` — Owner and Moderator for
179 `harness_owner` — because login upserts identity and perks but never
180 membership. The two id lists have to be edited together; the MNW side has a test
181 pinning the literals.
182
183 ### Resetting it
184
185 ```
186 deploy/reset-astra.sh
187 ```
188
189 Stops the service, drops every schema, re-migrates, re-seeds, and then asserts
190 the harness preconditions (three accounts, an Owner membership, threads
191 present). A run that logs in and finds no membership fails at the first write
192 with a 403, which reads like a permissions bug rather than an unfinished seed,
193 so the reset verifies rather than assuming.
194
195 This resets mt only. The identities live on testnot and come back from its own
196 reseed; the two are independent on purpose, because mt's state is disposable and
197 the accounts behind it are not.
198