sage: row 176 brief adds the credentials-body and project-root tests (mos-0878 Q1); row 178 brief rewritten for mos-0878 Q2: partial fix after the window rows, console-proof.token slot, ops-01 SealedSecret and middleware under R27, console-allowed-uid recorded as decision 101's uid and left alone

This commit is contained in:
2026-10-19 17:42:48 -05:00
parent 0750b6a495
commit 80cabd935d
2 changed files with 48 additions and 20 deletions
+35 -18
View File
@@ -1,6 +1,7 @@
# 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).
Vikunja #385, Gitea #1676. Owner Rocko or whichever seat is free, set at briefing. Gate owner Sage. Not on the window
path (mos-0874). Built after the window rows 169, 175 and 176, as a partial fix with the limit recorded (mos-0878 Q2).
Line numbers are at origin/refactor 835941b7c.
## The hole
@@ -42,32 +43,48 @@ signed-in request without the right value.
### 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.
The proof stops any process that can't read the proof file: another uid on the host, and a hostNetwork pod on the
k8s-woltje node (Cilium `AllowLocalhost=always`). It also stops a `run-task.sh` worker if its sandbox hides the host's
`~/.config`. The build checks that and records it in the packet, without assuming it.
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.
service change, so it's Jason's (Q173: infra). This row doesn't make that change. Agents acting as Jason only matter
once the stack is in use, so uid separation is a question for Jason before he says "in use". It doesn't hold up the
window. #385 stays open for his ruling (mos-0878 Q2).
### Jason's steps, not this row's
### `console-allowed-uid` isn't a proof
- 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.
`~/.config/mosaic-dev/secrets/mosaic-stack/console-allowed-uid` (0600, 65 bytes, 2026-10-12, contents not read) is the
Authentik uid for lead decision 101's switch from `--auth-header X-authentik-username --allow-user jason.woltje` to
`--auth-header X-authentik-uid --allow-user UID`. No code in the repo reads it. It's an operator input for that
reinstall (`scripts/webui-service.sh:145-146`, `docs/TOOLS.md:516`), and the live unit still signs in by username.
It's an identity, not a secret the proxy proves it holds. Once installed it sits in ExecStart, which any local process
can read. This row neither reuses it nor retires it, and the uid switch stays a separate operator step.
### Activation steps, not this row's
1. The secret. A slot file, `~/.config/mosaic-dev/secrets/mosaic-stack/console-proof.token`, mode 0600, 43 or more
characters from `[A-Za-z0-9_-]` (for example 32 random bytes in base64url). Under Q173 an agent may create it,
as long as the value is never printed, logged, put on argv or sent in a message (mos-0878 Q2). Create it with
`umask 077` and write it in one step, then check only `stat -c '%a %U %s'`.
2. Traefik. A SealedSecret holding the same value, and a middleware on the Console's route in the mosaic-preview Argo
app that sets the header (`headers.customRequestHeaders`, which replaces a client copy). That's ops-01's under R27.
It narrows who gets in and opens nothing new from outside (mos-0878). Sealing the value means handing it to ops-01
without printing it. The activation plan names that path, and Mos checks it before anything runs.
3. The unit. After a store backup, add `--proof-header NAME --proof-file <slot path>` to the Console unit with
`webui-service.sh install` and a restart, in a window Mos plan-checks. The order is middleware first, then the
unit flags, then a signed-in check through the public name and a forged loopback request that has to get 401.
Rollback drops the unit flags first, then the middleware. A STATE report follows. This step narrows who can reach
the Console from outside, and Q173 keeps any change to that for Jason. Mos rules at the plan check whether
narrowing counts. Until then it waits for Jason.
## 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.
2. A named request with no proof, a wrong proof, a proof one byte short, or a repeated proof header 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
@@ -90,5 +107,5 @@ Mutants, each killed by some gate test (mos-0862):
## 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
gate. Landing changes nothing live. The live unit gets the flags only through the activation steps above, after the
middleware.
@@ -104,9 +104,18 @@ move, stop and tell Sage. Before in-review, grep the tests for each changed path
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
9. The tester's `credentials` read never contains a token, a file path or a variable name (mos-0878 Q1). Give the
fixture's credential a known token value, a known file path and a known variable name. Assert that each string is
absent from the whole response body, through the broker and through the tester listener. Checking that the fields
are left out isn't enough.
10. A tester's `project.file` read stays inside the project root (mos-0878 Q1). Reads go through `git ls-tree` at a
commit, `documentPath` limits the path (`packages/project/src/paths.mjs:14`, `read.mjs:70`), and `blob()` takes
regular blobs only (`read.mjs:21-22`). Test through the tester path that `docs/../../x.md`, an absolute path and
`docs/%2e%2e/x.md` refuse. Test that a committed `docs/out.md` symlink pointing outside the repo refuses
`project-read-content`, and returns none of its target's bytes.
11. `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
12. 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):
@@ -117,6 +126,8 @@ Mutants, each killed by some gate test (mos-0862):
- `--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
- the credentials result passes `#secretCheck` but carries the fixture's file path
- `blob()` accepts mode 120000
## Gate