finish the offscale sweep: two real remnants and eleven stale names
Asked for a thorough check that nothing of offscale is left in the platform. The extraction was sound — no router, no permission entry, no app-store entry, no screen, no panel app, no env var anything reads. Two things were genuinely wrong, and both were in setup rather than in the server. ── The secret store minted a key for a plugin ── `officer-setup` bootstrapped two purposes: `jwt` and `headscale`. The second was a key for a PLUGIN, under a name the rename had already replaced, on a machine where the plugin is not installed and nothing would ever ask for it. offscale encrypts under `offscale`, and `getKey` creates on demand — which is precisely what lets core know nothing about plugins. Now core bootstraps `jwt` alone. ── `.env.example` documented variables for a deleted router ── HEADSCALE_URL, HEADSCALE_API_KEY and HEADSCALE_USER were described as driving `/api/vpn`. That router was deleted on 2026-08-14 and nothing has read any of them since. Left as a note saying where the tailnet's credentials live now, because somebody with an older `.env` will otherwise wonder why setting them does nothing. ── Names that outlived the thing ── Eleven comments referred to the plugin's route, panels, console or server registry as "headscale": three sidecar config files describing their own shape by pointing at it, two screens citing `/headscale` as the example of a Workspace/Panel route, the terminal's note about the Console, and the photos types. Also `catalogue.ts`, which still claimed headscale "is CORE now", and `working-on-officer.md`, which told the reader a core install runs six processes including `officer-headscale`. Upstream references are deliberately untouched: "Headscale's API", "the Headscale admin credential", "is this a Headscale server?" all name the thing the plugin drives, and renaming those would make the sentences false. `docs/secret-store.md` argued headscale must be core because the tailnet is the perimeter. Marked superseded rather than deleted, with the distinction that settles it: the perimeter is the tailnet EXISTING, which machine-setup still arranges on a core install — not the platform shipping the screen that administers it. Verified by counting: live code, setup provisioning, env references, permission entries, app-store entries, hono mounts, dock defaults, screens and panel apps all zero. What is left is generated files (Plugins.gen, plugin-schemas.gen), the bundled catalogue entry, test fixtures using "offscale" as an example app name, and history. tsgo clean, frontend builds, 873 pass / 7 fail unchanged, setup scripts parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+26
-15
@@ -10,10 +10,15 @@ A small SQLite database holding every encryption and signing key the platform us
|
||||
its own.
|
||||
|
||||
**One change from the design below: keys are per PURPOSE, not one key for everything.** The original
|
||||
plan moved a single at-rest key into the store. What shipped gives `headscale`, `wallet`, `photos`,
|
||||
plan moved a single at-rest key into the store. What shipped gives `offscale`, `wallet`, `photos`,
|
||||
`jellyfin`, `invoiceshelf`, `vault` and `service-connections` a key each, so one leaked key opens one
|
||||
plugin's columns rather than all seven. `jwt` is the eighth. A core install bootstraps two — `jwt` and
|
||||
`headscale` — and every other purpose is created when its plugin first asks.
|
||||
plugin's columns rather than all seven. `jwt` is the eighth. A core install bootstraps **one** — `jwt` —
|
||||
and every other purpose is created when its owner first asks.
|
||||
|
||||
(It bootstrapped `headscale` as well until 2026-08-16. That was a key for a plugin, minted under a name
|
||||
the rename had already replaced, on machines where the plugin was not installed and nothing would ever
|
||||
ask for it. `getKey` creates on demand, which is what makes core needing to know nothing about plugins
|
||||
work at all.)
|
||||
|
||||
---
|
||||
|
||||
@@ -25,8 +30,8 @@ not lost in a refactor:
|
||||
> **Secrets live in Postgres. The key that opens them does not.**
|
||||
|
||||
That is why `officer_db/src/crypto.ts` reads `VAULT_STORE_KEY` from the environment, and it is what
|
||||
makes `keys.ts:26` true: *"a stolen database dump is useless without .env, a stolen .env is useless
|
||||
without the passphrase"*.
|
||||
makes `keys.ts:26` true: _"a stolen database dump is useless without .env, a stolen .env is useless
|
||||
without the passphrase"_.
|
||||
|
||||
What is wrong is narrower, and it is about **blast radius across processes**.
|
||||
|
||||
@@ -46,13 +51,13 @@ The second problem is that changing the key is currently unrecoverable rather th
|
||||
Worth listing, because it is wider than the name suggests. Everything below is AES-256-GCM ciphertext in
|
||||
Postgres, encrypted through `officer_db/src/crypto.ts` with a key derived as `SHA-256(VAULT_STORE_KEY)`:
|
||||
|
||||
| column | what it is |
|
||||
| --- | --- |
|
||||
| `headscale_servers.api_key` | a Headscale **admin** credential — the schema notes it "can delete every node on a tailnet" |
|
||||
| `service_connections.secret` | every upstream credential the app store stores: gitea, memos, slskd, transmission |
|
||||
| `jellyfin_servers.access_token` | Jellyfin session token |
|
||||
| `wallets.config` | node credentials — macaroon, rune, LNDHub password, NWC URI. Spending authority |
|
||||
| `wallets.seed_envelope` | a BIP39 mnemonic, already sealed under an owner passphrase, encrypted **again** with this key |
|
||||
| column | what it is |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `headscale_servers.api_key` | a Headscale **admin** credential — the schema notes it "can delete every node on a tailnet" |
|
||||
| `service_connections.secret` | every upstream credential the app store stores: gitea, memos, slskd, transmission |
|
||||
| `jellyfin_servers.access_token` | Jellyfin session token |
|
||||
| `wallets.config` | node credentials — macaroon, rune, LNDHub password, NWC URI. Spending authority |
|
||||
| `wallets.seed_envelope` | a BIP39 mnemonic, already sealed under an owner passphrase, encrypted **again** with this key |
|
||||
|
||||
`decryptSecret` throws when the key does not verify, so a wrong key is not a degraded mode — it is every
|
||||
one of those becoming unreadable at once.
|
||||
@@ -69,7 +74,7 @@ persisted). Everything else in that table has exactly one lock.
|
||||
Keys cannot live in the database they unlock. A dump would then contain both the ciphertext and the
|
||||
thing that opens it, and the property quoted at the top stops being true. Encrypting the key with a
|
||||
second key only moves the question — eventually exactly one secret has to be readable without any other
|
||||
secret, and the only real decision is *where it lives*.
|
||||
secret, and the only real decision is _where it lives_.
|
||||
|
||||
SQLite rather than a flat file, for one reason that is not secrecy: **rotation needs key versions.** A
|
||||
rotation has to decrypt with the old key and re-encrypt with the new, and an interrupted rotation needs
|
||||
@@ -170,7 +175,13 @@ finds it already there.
|
||||
The core is what `ecosystem.light.config.cjs` runs today — `officer`, `officer-anthropic-proxy`,
|
||||
`officer-agent`, `officer-opencode`, `officer-pty` — **plus `officer-headscale`**.
|
||||
|
||||
Headscale is core for a stated reason rather than by preference: `CLAUDE.md` says the tailnet *is* the
|
||||
**Superseded 2026-08-15.** The tailnet left the repository and is the `offscale` plugin; core is the five
|
||||
above. The argument below was not refuted — it was overtaken. What settled it: the perimeter is the
|
||||
tailnet EXISTING, which is a machine-setup concern, and that is not the same as the platform shipping the
|
||||
screen that administers it. `machine-setup.sh` still walks you onto a tailnet on a core install; only the
|
||||
admin UI became optional. Kept because the reasoning is what a future "should X be core?" argument needs.
|
||||
|
||||
Headscale is core for a stated reason rather than by preference: `CLAUDE.md` says the tailnet _is_ the
|
||||
perimeter — origin checking was removed on 2026-08-13 precisely because the tailnet is what stands in
|
||||
its place, so the tailnet is now load-bearing rather than one layer of two. A security model
|
||||
that rests on the tailnet cannot treat administering the tailnet as an optional extra. Vaultwarden and
|
||||
@@ -220,7 +231,7 @@ Two properties it must have, both learned from the failure it replaces:
|
||||
|
||||
- **Transactional.** A half-rotated table is worse than either end state, because nothing afterwards can
|
||||
tell which rows are which.
|
||||
- **Verify before writing.** Every row must decrypt with the retired key *before* anything is written.
|
||||
- **Verify before writing.** Every row must decrypt with the retired key _before_ anything is written.
|
||||
A key that is already wrong should fail loudly on row one rather than produce a second layer of
|
||||
unreadable data.
|
||||
|
||||
|
||||
@@ -40,8 +40,10 @@ prose, here or elsewhere.
|
||||
|
||||
## What is switched off (2026-08-13)
|
||||
|
||||
A core install runs **six** pm2 processes: `officer`, `officer-anthropic-proxy`,
|
||||
`officer-claude-code`, `officer-opencode`, `officer-pty`, `officer-headscale`. Everything else is a
|
||||
A core install runs **five** pm2 processes: `officer`, `officer-anthropic-proxy`,
|
||||
`officer-claude-code`, `officer-opencode`, `officer-pty` — the list in `officer-setup`'s
|
||||
`CORE_PROCESSES`. `officer-headscale` was the sixth until 2026-08-15; the tailnet is the `offscale`
|
||||
PLUGIN now, so it runs as `officer-offscale` only where it has been installed. Everything else is a
|
||||
plugin, and every plugin router is commented out in `hono.ts` with its permission's `api` claim
|
||||
commented beside it — they must move together or `assertPermissionTotality` refuses to boot.
|
||||
|
||||
@@ -95,7 +97,8 @@ Commit messages: simple lowercase, no prefixes, explaining _why_.
|
||||
|
||||
The server runs under pm2 as `officer`, plus sidecars (`officer-anthropic-proxy`, `officer-claude-code`,
|
||||
`officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
|
||||
`officer-slskd`, `officer-headscale`, `officer-transmission`, `officer-invoiceshelf`, `officer-wallet`).
|
||||
`officer-slskd`, `officer-transmission`, `officer-invoiceshelf`, `officer-wallet`, and a process per
|
||||
installed plugin — `officer-offscale`, `officer-music`).
|
||||
`pm2 list` shows them; `pm2 logs officer` follows.
|
||||
|
||||
Two of those names are worth knowing apart: **`officer-anthropic-proxy` holds the Anthropic credential
|
||||
|
||||
Reference in New Issue
Block a user