Stop embedding the control token in every page (v0.9.10)

Closes TODO 2 of the security audit in docs/prs/2026-07-08-PR#000.

Every rendered page carried the live control token in a meta tag so that
gittally.js could send it, but no GET is authenticated — so `curl … |
grep gittally-control-token` handed the token to anyone, and read access
was effectively write access.

Reading stays fully public, which is a requirement rather than an
oversight: build states, logs and artifacts must be linkable from Gitea,
chats or tickets without a login. Only the distribution of the token
changed. The meta tag is gone; gittally.js keeps the token in
localStorage and asks for it once per browser, so knowing it requires
shell access to `.git/gittally/control-token` on the host. A token the
server rejects is dropped and asked for once more, so a rotated secret is
not a dead end. As a request header it stays inherently CSRF-safe.

The five branches of that flow (first use, reuse, stale token, cancelled
prompt, wrong token twice) were exercised against the real source with a
throwaway node harness; the UI test now asserts the token does not appear
in the rendered page. `docs/deployment.md` gained a "Control Token"
section on the public-read/token-protected-write split.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-11 08:00:09 +02:00
co-authored by Claude
parent ea0b67d331
commit a734d91918
9 changed files with 103 additions and 16 deletions
+20
View File
@@ -92,6 +92,26 @@ systemctl --user stop gittally-<repo-name>.service # stop
To update GitTally, replace `~/bin/gittally.jar` and restart the service.
## Control Token
Viewing is public by design: build states, logs and artifacts are readable without any login, so they can be linked from Gitea, chats or tickets.
Only the three mutating actions — restart, cancel, delete — require the control token from `.git/gittally/control-token`, a random secret the server generates on first start (mode `0600`; delete the file to rotate it).
The token is never embedded in a page.
The first time you press one of the control buttons, the browser asks for it once and keeps it in `localStorage` for that browser; a rejected token is dropped and asked for again.
Read it on the host:
```bash
cat ~/<repo>/.git/gittally/control-token
```
For scripts, pass it as a header — it is not accepted as a query parameter, because URLs end up in access logs and browser history:
```bash
curl -X POST -H "X-GitTally-Token: $(cat .git/gittally/control-token)" \
"https://ci.example.org/api/builds/restart?branch=main"
```
## Environment File
`.git/gittally/gittally.env` is loaded by the unit as `EnvironmentFile`.