sage: briefs for rows 176 (#1674, Console tester listener on 127.0.0.1, 7332 refuses x-mosaic-tester, mos-0872/0874), 177 (#1675, tester threads, scoping) and 178 (#1676, Traefik proof for signed-in requests, Vikunja #385, mos-0874)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# Brief: the Console checks that a signed-in request came through Traefik
|
||||
|
||||
Vikunja #385, Gitea #1676. Owner and reviewer are set at briefing. Gate owner Sage. Not on the window path (mos-0874).
|
||||
Line numbers are at origin/refactor 835941b7c.
|
||||
|
||||
## The hole
|
||||
|
||||
In preview mode, the Console treats a request as Jason's when the Host is the public name and the auth header (live:
|
||||
`X-authentik-username`) holds exactly the allowed user (`packages/webui/src/serve.mjs:453`, :466-469). Traefik and
|
||||
authentik set that header after sign-in and strip client copies. But the tunnel delivers every public request to
|
||||
127.0.0.1:7332 (`R:0.0.0.0:7332:127.0.0.1:7332`), so the Console can't tell Traefik's requests from a local
|
||||
process's. Any process on dragon-lin that can open 127.0.0.1:7332 can send `Host: <public name>` and
|
||||
`X-authentik-username: jason.woltje` and get Jason's sends, saves and settings writes. An agent can too, which gets
|
||||
around the human-shell guard (decisions 62 and 94).
|
||||
|
||||
## The design
|
||||
|
||||
Traefik adds a proof header with a secret value to every request it forwards to the Console. The Console refuses a
|
||||
signed-in request without the right value.
|
||||
|
||||
### `packages/webui/src/serve.mjs` and `cli.mjs`
|
||||
|
||||
- New serve flags `--proof-header NAME` and `--proof-file PATH`, given together and only with the preview flags.
|
||||
Either one alone refuses to start (exit 2).
|
||||
- The Console reads PATH once at start. It refuses to start (exit 2) unless the file is a regular file owned by the
|
||||
service's uid, has mode 0600 or 0400, and holds one line of at least 32 bytes from `[A-Za-z0-9_-]`. The value
|
||||
never appears on argv, in a log line, in an error message or in `/api/config`.
|
||||
- On a request with the public Host, the proof check runs before the auth header is read. It needs exactly one copy
|
||||
of NAME, and the value has to match by `crypto.timingSafeEqual` over equal-length buffers. Anything else answers
|
||||
401 `proxy proof required`, with no bus call and no board call. A missing value and a wrong value get the same
|
||||
answer.
|
||||
- A request with NAME and any other Host refuses 403, as the auth header does at :456.
|
||||
- The tester listener from row 176 refuses NAME 403.
|
||||
- Without the flags, the startup line says `proxy proof off`, so the live unit's state is visible in its journal.
|
||||
|
||||
### `scripts/webui-service.sh` and `scripts/stack-release.sh`
|
||||
|
||||
- Both flags join FLAGS, with the same checks. `render` refuses a PATH inside the repo or a copy, and a PATH it can't
|
||||
stat. It never reads the file.
|
||||
- `stack-release.sh` carries both flags through a re-pin and a rollback unchanged (`P_CONSOLE`).
|
||||
- Update `packages/webui/README.md`, `docs/TOOLS.md` and the unit template's header comment.
|
||||
|
||||
### What this stops and what it doesn't
|
||||
|
||||
The proof stops any process that can't read the proof file: another uid on the host, a hostNetwork pod on the
|
||||
k8s-woltje node (Cilium `AllowLocalhost=always`), and a sandboxed worker, since `run-task.sh` workers get no host
|
||||
home.
|
||||
|
||||
It doesn't stop a process running as the Console's own uid. The Console runs as a `systemctl --user` unit under
|
||||
jwoltje, and every T3 seat runs as jwoltje too. A seat can read any file the Console can read, and `ptrace_scope` 1
|
||||
doesn't stop that. Closing that part means the Console runs under a uid the seats don't have, which is a host and
|
||||
service change, so it's Jason's (Q173: infra). This row doesn't make that change. The brief records it, and the issue
|
||||
stays open until Jason rules on it.
|
||||
|
||||
### Jason's steps, not this row's
|
||||
|
||||
- The secret's home. A file outside git, mode 0600, outside every repo and copy, written by Jason. Its path goes in
|
||||
the unit through `--proof-file`. No agent writes or reads it, and it never goes in a message.
|
||||
- The Traefik middleware that sets the header on the Console's route (`headers.customRequestHeaders`, which replaces
|
||||
a client copy). That's k3s, so it's Jason's. The value has to match the file.
|
||||
- Turning it on in the live unit. That changes who can reach the Console from outside (it narrows it), so it waits
|
||||
for Jason under Q173. The order is middleware first, then unit flags, then a signed-in check. Rollback drops the
|
||||
unit flags.
|
||||
|
||||
## Tests
|
||||
|
||||
1. A named request with the right proof and auth header passes, and the reads and the send path still work as before.
|
||||
2. A named request with no proof, a wrong proof, a proof one byte short, a repeated proof header, or the proof in
|
||||
another case of header name gets 401 with no bus call and no board call. Assert that the body never contains the
|
||||
value.
|
||||
3. NAME with a loopback Host or any other Host refuses 403. NAME on the row 176 tester listener refuses 403.
|
||||
4. Start-up refusals: one flag without the other; the flags without preview mode; a missing file, a directory, a
|
||||
symlink, mode 0644, another owner, a short value, a value with a newline in the middle or a bad character. Each
|
||||
one exits 2, and stderr never shows the value.
|
||||
5. The value never shows in the request log, in `/api/config`, or in `ps` output for the serve process. The test reads
|
||||
`/proc/<pid>/cmdline`.
|
||||
6. `webui-service.sh render` puts both flags in ExecStart, refuses a PATH inside the repo, and doesn't open the file.
|
||||
`stack-release.sh` keeps both flags across a re-pin and a rollback.
|
||||
7. A timing check isn't a gate test. The code uses `timingSafeEqual`, and a review grep confirms it.
|
||||
|
||||
Mutants, each killed by some gate test (mos-0862):
|
||||
|
||||
- the auth header is read before the proof check
|
||||
- a wrong proof with the right length passes
|
||||
- a missing proof passes when the flags are set
|
||||
- the proof is accepted with a loopback Host
|
||||
- mode 0644 accepted
|
||||
- the value is logged on refusal
|
||||
|
||||
## Gate
|
||||
|
||||
The reviewer approves the candidate manifest. Webui and cli suites and every `scripts/test-*.sh` green on Sage's
|
||||
gate. Landing changes nothing live. The live unit gets the flags only in a window Jason runs or approves, after the
|
||||
middleware.
|
||||
@@ -0,0 +1,126 @@
|
||||
# Brief: a loopback-only Console listener for the tester
|
||||
|
||||
Gitea #1674. Owner Rocko, reviewer Dewey, gate owner Sage. Starts after rows 169 and 175 are done: it uses 175's
|
||||
tester writes and touches `scripts/webui-service.sh`, which 169's window path reads. Line numbers are at
|
||||
origin/refactor 1c90c68c5.
|
||||
|
||||
## Why
|
||||
|
||||
DECISIONS Q173 makes the dragon-lin mosaic-stack business a test system that agents test end to end, as the tester
|
||||
and never as Jason (mos-0870 N3). The merged window's browser check has to run against the Console without Jason
|
||||
signing in. Mos's design (mos-0872) is a second listener for the tester, because the loopback Host check on 7332
|
||||
can't tell a tester call from a public one:
|
||||
|
||||
- `mosaic-preview-tunnel` runs chisel with `R:0.0.0.0:7332:127.0.0.1:7332`, so all public traffic reaches the
|
||||
Console from 127.0.0.1 with whatever Host the client sends.
|
||||
- Cilium's `AllowLocalhost=always` lets the k8s-woltje node's hostNetwork pods reach 7332 too.
|
||||
|
||||
The namespace `woltje-mosaic-preview` has a default-deny policy and a tunnel policy that admits 7332 only from
|
||||
namespace traefik, and ops-01 confirmed it blocks other pods (Vikunja #384 comment 285, closed; mos-0874 corrects
|
||||
mos-0872 on this). That doesn't change the design.
|
||||
|
||||
So the tester gets its own listener on 127.0.0.1 and a port the tunnel doesn't forward, and 7332 refuses anything that
|
||||
asks to be the tester. Who can reach the Console from outside doesn't change: the tunnel still forwards 7332 only.
|
||||
Other users on the host can reach a loopback port, as they can reach 7332's loopback path today; that is P19 333 and
|
||||
this row doesn't change it. A local process that forges the preview's auth header on 7332 is Vikunja #385, row 178,
|
||||
and not this row.
|
||||
|
||||
## The change
|
||||
|
||||
### `packages/webui/src/cli.mjs`
|
||||
|
||||
- New flag `serve --tester-port N`. N is a whole number in 1024..65535, not equal to `--port`, and not 7332 whatever
|
||||
`--port` is, since the tunnel forwards 7332. Anything else refuses to start (exit 2).
|
||||
- `--tester-port` needs the bus configured, as `--send` does, and refuses to start without it.
|
||||
- With the flag, `serve` starts a second `startServer` in the same process on `127.0.0.1:N`, whose bus caller is
|
||||
`busCaller({ ..., cli: TESTER_CLI })`. `TESTER_CLI` is exported from `packages/cli/src/transport.mjs:13`, and
|
||||
`busCaller` already takes `cli` (`packages/webui/src/bus.mjs:60`), so `bus.mjs` doesn't change. `tester-cli.mjs`
|
||||
sets its own nonce (`packages/bus/src/tester-cli.mjs:10-16`).
|
||||
- The tester listener's writes are the tester writes from row 175 only: `instancesWrite` (`instances.set`),
|
||||
`routesWrite` (`routes.set`) and `nameWrite` (`name.set`). `send`, `settingsWrite`, `projectSave` and
|
||||
`assistantStop` stay null on it, and so does `chat` until row 177.
|
||||
- The startup lines name the tester listener's address and say it binds the tester.
|
||||
|
||||
### `packages/webui/src/serve.mjs`
|
||||
|
||||
- `startServer` takes `tester: true` for the second listener. With it, `preview` must be null (refuse to start
|
||||
otherwise), and every request needs the loopback Host on the listener's own port, which the non-named check at
|
||||
:458-461 already does.
|
||||
- On the tester listener, `/api/instances`, `/api/routes` and `/api/name` pass without `named`, and take the same body
|
||||
checks as the human's routes plus Origin `http://127.0.0.1:N` and `Sec-Fetch-Site: same-origin`. Their log lines
|
||||
say `user="tester"`.
|
||||
- On the tester listener, `/api/settings` POST, `/api/project-file` POST, `/api/assistant-stop`, `/api/bus/message`
|
||||
and every `/api/chat` write refuse 403 with no bus call.
|
||||
- The tester listener refuses 403 any request that carries the main listener's `--auth-header` header, so a
|
||||
misrouted proxy request never binds the tester.
|
||||
- Every listener that isn't the tester listener, 7332 included, refuses 403 any request with an `x-mosaic-tester`
|
||||
header, whatever the Host, before any other check reads the request (mos-0872).
|
||||
- `/api/config` on the tester listener answers `tester: true`, `send: false`, `edit: false`, and the page shows a
|
||||
tester banner and the instance, routes and name editors only.
|
||||
- A broker refusal of `tester-off` or `tester-required` maps to 403 in `busStatus`, and the page says the tester is
|
||||
off.
|
||||
|
||||
### Bus reads the pages need
|
||||
|
||||
The Console reads `inbox`, `tasks`, `agents`, `trail`, `decisions`, `credentials`, `replies`, `messages`, `usage`,
|
||||
`limits`, `names`, `instances` and `routes` (`serve.mjs:59`), plus `project.files` and `project.file` for the docs
|
||||
page. Row 175 leaves `credentials`, `routes`, `project.files` and `project.file` refusing a tester. This row adds
|
||||
them as tester reads in `packages/bus/src/broker.mjs`:
|
||||
|
||||
- `credentials` joins `TESTER_VERBS`. It already runs for a reader (:2158), and `#secretCheck` still runs on the
|
||||
result.
|
||||
- `routes` passes the organization branch for a tester, as row 175's three reads do.
|
||||
- `#requestProjectFile` (:1400) refuses a tester through `this.#human(s)` (:1404). Let a tester read `project.files`
|
||||
and `project.file`. `project.file.save` stays
|
||||
human-only (`project-spec-human-only`).
|
||||
|
||||
### `scripts/webui-service.sh`
|
||||
|
||||
- `--tester-port N` joins FLAGS, with the same checks as serve's, rendered into ExecStart. `render`, `install` and
|
||||
`status` show it.
|
||||
- `scripts/stack-release.sh` carries the pointer's Console flags as an array (`P_CONSOLE`, :96, :704, :708). Check it
|
||||
carries `--tester-port` through a re-pin and a rollback unchanged, and add the test.
|
||||
- Update the header comment, `docs/TOOLS.md` and `packages/webui/README.md`.
|
||||
|
||||
### Untouched
|
||||
|
||||
Row 164 pins `packages/bus/src/human.mjs`, `packages/bus/src/human-cli.mjs`, `packages/cli/src/transport.mjs` and
|
||||
`packages/webui/src/bus.mjs` (`packages/bus/tests/channel-proof.test.mjs:11-19`). Don't touch them. If one has to
|
||||
move, stop and tell Sage. Before in-review, grep the tests for each changed path and for a 64-hex pin on it.
|
||||
|
||||
## Tests
|
||||
|
||||
1. 7332's refusal: a request with `x-mosaic-tester` on the main listener refuses 403 with a loopback Host, with the
|
||||
preview's public Host and auth header, and with any other Host. No bus call, no board call.
|
||||
2. The tester listener binds 127.0.0.1 only. `--tester-port` equal to `--port`, equal to 7332, below 1024, above
|
||||
65535 or not a number refuses to start, and so does a missing bus config.
|
||||
3. The tester listener refuses a non-loopback Host, a Host with another port, and the main listener's auth header,
|
||||
each with 403.
|
||||
4. Every bus read on the tester listener goes through `TESTER_CLI` (assert the `cli` the caller spawns), and none
|
||||
through `HUMAN_CLI`.
|
||||
5. Against a real broker fixture with `bus.tester` true: an instance change, a routes change and a name set through
|
||||
the tester listener land with actor `tester` (receipt) and `by = 'tester'`.
|
||||
6. The tester listener refuses settings save, project-file save, assistant stop, bus message and chat writes with
|
||||
403 and no bus call.
|
||||
7. With `bus.tester` off, the tester listener's reads answer 403 `tester-off`.
|
||||
8. A tester reads `credentials`, `routes`, `project.files` and `project.file`, and still refuses `project.file.save`.
|
||||
9. `webui-service.sh render --tester-port 7333` puts it in ExecStart, and refuses 7332, the main port and bad values
|
||||
(exit 4). `stack-release.sh` keeps `--tester-port 7333` across a re-pin and a rollback.
|
||||
10. A browser test (the webui browser harness) opens the tester listener, sees the tester banner, changes an
|
||||
instance's route, and the change lands as tester.
|
||||
|
||||
Mutants, each killed by some gate test (mos-0862):
|
||||
|
||||
- drop the `x-mosaic-tester` refusal on the main listener, or check it only for a loopback Host
|
||||
- the tester listener calls through `HUMAN_CLI`
|
||||
- the tester listener lets a settings save or a project-file save through
|
||||
- `--tester-port 7332` accepted
|
||||
- the tester listener accepts the main listener's auth header
|
||||
- `credentials` dropped from the tester reads, or `project.file.save` let through for a tester
|
||||
|
||||
## Gate
|
||||
|
||||
Dewey approves the candidate manifest. Bus, cli, webui and business suites and every `scripts/test-*.sh` green on
|
||||
Sage's gate. The merged window adds `--tester-port 7333` to the Console unit after a store backup, and the window's
|
||||
browser check runs against http://127.0.0.1:7333/. Before it does, the window reads the tunnel's forward spec and
|
||||
stops unless it still forwards 7332 only.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Brief: tester threads (scoping)
|
||||
|
||||
Gitea #1675. Proposed owner Dewey, reviewer Filbert, gate owner Sage. Starts after row 176 is done. Line numbers are at
|
||||
origin/refactor 835941b7c. This is a scoping brief. The owner writes the design into the packet, and Sage approves it
|
||||
before any code.
|
||||
|
||||
## Why
|
||||
|
||||
Q173 says agents test the test system end to end as the tester (mos-0870). The Console's chat page and the window's
|
||||
W6/W7 checks need a thread conversation. Every thread verb in `packages/bus/src/broker.mjs` is human-only today:
|
||||
`thread.create` (:2282), `thread.list` (:2306), `thread.open` (:2320), `thread.send` (:2327) and
|
||||
`thread.messages` (:2354) each start with `this.#human(s)`. They record the human as the thread's owner,
|
||||
`thread_opens.by` and the message's `from_role 'human'` and `from_run 'human-cli'`.
|
||||
|
||||
## What the design has to settle
|
||||
|
||||
1. A tester thread's owner is `tester`, and a tester sees and opens only threads it owns. The human's thread list
|
||||
leaves tester threads out, or labels them. Pick one and say why.
|
||||
2. `thread.send` from a tester stores `from_role 'tester'`, and `from_run` is the tester CLI, never `human-cli`. Row
|
||||
120's messaging lists have to allow tester to instance. Say whether that is a business-file entry or a broker
|
||||
rule.
|
||||
3. Replies on a tester thread route to `tester` and never to the human's inbox. The human's view shows them as tester
|
||||
traffic.
|
||||
4. Whether a tester gets `assistant.stop` on its own threads only. Never on the human's.
|
||||
5. The `human.input` event on `thread.send` (:2351) has to say tester, or a tester thread needs its own event kind.
|
||||
Check what the launcher and the runner do with the event first.
|
||||
6. The Console's tester listener (row 176) gets `/api/chat` for tester threads only, and 7332's chat stays the
|
||||
human's.
|
||||
7. DDL: read `threads`, `thread_opens` and `messages` in the migrations for CHECKs before saying no schema change.
|
||||
|
||||
## Untouched
|
||||
|
||||
The row 164 pins (`packages/bus/tests/channel-proof.test.mjs:11-19`). Row 175's tester writes and row 176's
|
||||
listener, except where point 6 adds chat.
|
||||
|
||||
## Gate
|
||||
|
||||
Set in the design. At least the bus, webui and conversation suites and every `scripts/test-*.sh`.
|
||||
Reference in New Issue
Block a user