Header-only control token, masked secrets, loopback default (v0.9.9)
Finishes the small items of the security audit in docs/prs/2026-07-08-PR#000: TODO 3, 4 and 7. The three mutating endpoints of BuildsApiController no longer accept the control token as a `token` query parameter — only the X-GitTally-Token header, which the bundled UI has always used. URLs end up in access logs, proxy logs, browser history and Referer headers, and the token never expires, so a historical log capture would yield a valid credential. `config:print` masks git.token as `***` on both the raw and the --full path and names the new --show-secrets flag in a leading YAML comment, so the output stays parseable when piped. The setup script points at --show-secrets where it used to steer the operator to the plain token. `server.bindAddress` now defaults to 127.0.0.1: neither the UI nor the API authenticates read access, so reaching GitTally should require the host's reverse proxy. Existing .gittally.yml files keep their explicit value; the managed nginx container needs `0.0.0.0` set deliberately, which is noted in the release notes, docs/configuration.md and docs/deployment.md. Released as v0.9.9, which also carries the previous two commits. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+13
-2
@@ -38,6 +38,9 @@ java -jar build/libs/gittally.jar config:print # only explicitly set val
|
||||
java -jar build/libs/gittally.jar config:print --full # all values including defaults
|
||||
```
|
||||
|
||||
`git.token` is masked as `***` by default, so the output can safely be shared or pasted.
|
||||
Add `--show-secrets` to print it in clear text.
|
||||
|
||||
## `.gittally.yml`
|
||||
|
||||
Values shown are the defaults.
|
||||
@@ -48,8 +51,9 @@ server:
|
||||
publicBaseUrl: https://ci.example.org/
|
||||
# HTTP port of the `server` subcommand (default 18080, like legacy)
|
||||
port: 18080
|
||||
# bind address of the `server` subcommand
|
||||
bindAddress: 0.0.0.0
|
||||
# bind address of the `server` subcommand; loopback only, because the UI and the API
|
||||
# are unauthenticated — set 0.0.0.0 only deliberately (see the note below)
|
||||
bindAddress: 127.0.0.1
|
||||
# optional Impressum (legal disclosure) link in the web UI footer; empty hides the link
|
||||
impressumUrl: ""
|
||||
# Opt-in managed nginx+certbot Docker container for HTTPS, for hosts without
|
||||
@@ -172,6 +176,13 @@ branches:
|
||||
- "04:00"
|
||||
```
|
||||
|
||||
### Notes on `server.bindAddress`
|
||||
|
||||
The default is `127.0.0.1`.
|
||||
Neither the web UI nor the JSON API authenticates read access, and every page carries the control token that unlocks the build controls, so GitTally is meant to sit behind the host's reverse proxy rather than on a public interface.
|
||||
Set `0.0.0.0` only deliberately — for the managed nginx container (which reaches GitTally over the Docker bridge, not over loopback), or when the proxy runs on another host.
|
||||
Installations created before v0.9.9 have `bindAddress: 0.0.0.0` written into their `.gittally.yml` and keep it; the new default only applies where the key is absent or `init` writes a fresh file.
|
||||
|
||||
### Notes on `server.nginx`
|
||||
|
||||
With `nginx.enabled`, the `server` subcommand also starts a managed nginx Docker container that serves GitTally over HTTPS (ADR 0005).
|
||||
|
||||
+2
-2
@@ -101,7 +101,7 @@ All GitTally configuration lives in the YAML files described in [configuration.m
|
||||
|
||||
## Reverse Proxy (nginx)
|
||||
|
||||
Bind GitTally to localhost and set the public URL in `.gittally.yml`:
|
||||
Bind GitTally to localhost — the default since v0.9.9 — and set the public URL in `.gittally.yml`:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
@@ -197,5 +197,5 @@ All nginx and certificate failures are non-fatal warnings — the plain HTTP ser
|
||||
|
||||
`serverName` must be a public DNS name pointing at the host, reachable from the internet on port 80/443 (directly or via a port forward to `httpPort`/`httpsPort`), otherwise the ACME challenge fails.
|
||||
The nginx container cannot reach `localhost` of the host, so the proxy upstream defaults to `serverName`; set `server.nginx.upstreamHost` if the host is reachable under a different name from inside containers.
|
||||
With the managed nginx, keep `server.bindAddress: 0.0.0.0` (or an address reachable from the Docker network) — binding GitTally to `127.0.0.1` would make it unreachable for the proxy.
|
||||
With the managed nginx, set `server.bindAddress: 0.0.0.0` explicitly (or an address reachable from the Docker network) — the default `127.0.0.1` makes GitTally unreachable for the proxy container.
|
||||
See [configuration.md](configuration.md) for all `server.nginx.*` keys.
|
||||
|
||||
@@ -65,7 +65,7 @@ Blast radius is limited to build-lifecycle operations (a DoS/integrity concern,
|
||||
|
||||
#### TODO 3 — Accept the control token via header only
|
||||
|
||||
- [ ] Remove the `token` query-parameter variant from the three mutating endpoints in [`BuildsApiController.kt:90,115,129`](../../src/main/kotlin/de/hoennig/gittally/server/BuildsApiController.kt); keep only the `X-GitTally-Token` header (which the bundled UI already uses).
|
||||
- [x] Remove the `token` query-parameter variant from the three mutating endpoints in [`BuildsApiController.kt:90,115,129`](../../src/main/kotlin/de/hoennig/gittally/server/BuildsApiController.kt); keep only the `X-GitTally-Token` header (which the bundled UI already uses).
|
||||
|
||||
**Background.**
|
||||
Tokens in URLs are routinely written to access logs, reverse-proxy logs, browser history, and the `Referer` header on outbound navigation.
|
||||
@@ -74,8 +74,9 @@ The query-param path exists only for legacy convenience.
|
||||
|
||||
#### TODO 4 — Redact the Gitea token in `config:print`
|
||||
|
||||
- [ ] Mask `git.token` (and any future secret) by default in [`ConfigPrintCommand.kt:20-31`](../../src/main/kotlin/de/hoennig/gittally/commands/ConfigPrintCommand.kt); gate the plaintext value behind an explicit `--show-secrets` flag.
|
||||
- [ ] Update `tools/setup-gittally-instance:272`, which currently steers the operator to run `config:print --full` to view the token.
|
||||
- [x] Mask `git.token` (and any future secret) by default in [`ConfigPrintCommand.kt:20-31`](../../src/main/kotlin/de/hoennig/gittally/commands/ConfigPrintCommand.kt); gate the plaintext value behind an explicit `--show-secrets` flag.
|
||||
Masked as `***` on both the `--full` and the raw path, with a leading YAML comment naming the flag, so the output stays parseable when piped.
|
||||
- [x] Update `tools/setup-gittally-instance:272`, which currently steers the operator to run `config:print --full` to view the token.
|
||||
|
||||
**Background.**
|
||||
Both the `--full` and default branches print the token verbatim to stdout, landing it in terminal scrollback, `script(1)` captures, screen-shares, or CI logs.
|
||||
@@ -112,7 +113,8 @@ Before this PR all build config was loaded from the primary checkout via `config
|
||||
|
||||
#### TODO 7 — Default `bindAddress` to `127.0.0.1`
|
||||
|
||||
- [ ] Change the default in [`GitTallyConfig.kt:21`](../../src/main/kotlin/de/hoennig/gittally/config/GitTallyConfig.kt) and the `init` template ([`InitCommand.kt:126`](../../src/main/kotlin/de/hoennig/gittally/commands/InitCommand.kt)) from `0.0.0.0` to `127.0.0.1`; require operators to opt into all-interfaces.
|
||||
- [x] Change the default in [`GitTallyConfig.kt:21`](../../src/main/kotlin/de/hoennig/gittally/config/GitTallyConfig.kt) and the `init` template ([`InitCommand.kt:126`](../../src/main/kotlin/de/hoennig/gittally/commands/InitCommand.kt)) from `0.0.0.0` to `127.0.0.1`; require operators to opt into all-interfaces.
|
||||
Shipped as v0.9.9 with the migration note in the release notes, `docs/configuration.md` ("Notes on `server.bindAddress`") and `docs/deployment.md`: existing configs keep their explicit value, and the managed nginx now needs `0.0.0.0` set deliberately.
|
||||
|
||||
**Background.**
|
||||
The current default binds all interfaces, which — combined with the public read surface and token-in-HTML — exposes the whole UI and the control token to the network whenever the reverse proxy is forgotten.
|
||||
@@ -149,7 +151,7 @@ A small TOCTOU gap; `GitAskPass`'s atomic-at-creation approach is the pattern to
|
||||
- TODO 2: full fix (gating the pages) versus documentation-only — the pages are the UI, so gating them needs a decision on how operators authenticate; the current implemented behavior is fully public.
|
||||
- TODO 5: whether public read access is acceptable by design (it matches legacy) or should change; current behavior leaves all reads public.
|
||||
- TODO 6: the pinned set is settled (secrets + Gitea/server + `docker.enabled`/`docker.network`); the open point is whether `docker.env` should also be pinned, since a branch overriding it controls its own container's environment (currently proposed as worktree-overridable).
|
||||
- TODO 7: changing the default `bindAddress` is a behavior change for existing installs that rely on `0.0.0.0`; needs a migration note.
|
||||
- ~~TODO 7: changing the default `bindAddress` is a behavior change for existing installs that rely on `0.0.0.0`; needs a migration note.~~ Settled: the default changed in v0.9.9 and the migration note is in the release notes and both deployment docs.
|
||||
|
||||
## Additional Changes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user