diff --git a/docs/infrastructure/ci-gitea.md b/docs/infrastructure/ci-gitea.md index 6d99f694..871eec78 100644 --- a/docs/infrastructure/ci-gitea.md +++ b/docs/infrastructure/ci-gitea.md @@ -462,3 +462,82 @@ jobs: name: e2e-results path: frontend/test-results/e2e/ ``` + +--- + +## Renovate + Nightly Audit — Token Model + +> See [ADR-041](../adr/041-renovate-runner-setup.md) for full rationale. + +### Two-token model + +This repo uses two separate tokens for automated dependency work, both manually +provisioned as Gitea secrets. There is **no auto-provided `GITEA_TOKEN`** on +self-hosted Gitea runners — it must be created manually (see §Context Variable Names +table above). + +| Secret | Scopes | Job | Reason | +|--------|--------|-----|--------| +| `RENOVATE_TOKEN` | `contents` + `pull_request` + `issues` | `renovate.yml` | Renovate needs to read/write files and open PRs | +| `NIGHTLY_AUDIT_TOKEN` | `issues` only | `nightly.yml` → `npm-audit` job | Only needs to file a tracking issue; an `issues`-only token cannot push branches or read contents — limits blast radius on leak | + +Both tokens belong to a single dedicated bot account. **Branch protection on `main` +must forbid the bot pushing directly**, because a `contents`-scoped token can push +to any unprotected branch. + +### PAT rotation cadence + +Rotate both tokens: +- **Annually** (calendar reminder) +- **Immediately** on suspected compromise (runner log leak, accidental `set -x`, etc.) + +Tokens are stored exclusively as Gitea secrets. Never commit them to `.env` files or +log them. The nightly audit step passes its token via `env:` at the step level, reads +it as `$NIGHTLY_AUDIT_TOKEN` in the shell, and never runs the API `curl` under +`set -x`. + +### OSV vs platform alerts on Gitea + +Renovate's `vulnerabilityAlerts` config key requires a *platform* vulnerability graph. +**Gitea does not expose one** — it has no equivalent to the GitHub Advisory Database +API. On this runner, `vulnerabilityAlerts` is a label carrier only: it attaches +`security` and `P1-high` labels to PRs that `osvVulnerabilityAlerts` already raised. + +`osvVulnerabilityAlerts: true` is the load-bearing detector. Renovate queries +[OSV.dev](https://osv.dev) directly, which works regardless of platform. The runner +host must be able to reach `osv.dev:443`. If egress is filtered and OSV.dev is +unreachable, the flag silently no-ops — verify egress when standing up the runner. + +### Nightly audit vs PR gate (divergence) + +| Gate | Command | Dev deps | When | +|------|---------|----------|------| +| PR gate (`ci.yml`) | `npm audit --audit-level=high --omit=dev` | ❌ excluded | Every PR | +| Nightly audit (`nightly.yml`) | `npm audit --audit-level=high` | ✅ included | Nightly + `workflow_dispatch` | + +The nightly job is **deliberately broader** — it catches dev-tooling advisories +(esbuild, Vite, vitest, etc.) that the PR gate ignores. A red nightly audit job does +**not** mean the PR gate is broken; the two signals are independent. + +### Runbook: nightly-opened tracking issue + +When the `npm-audit` job opens or updates the tracking issue +"Nightly npm audit: high-severity advisory": + +1. **Triage severity.** Check the advisory page (link in the issue body). Is it + exploitable in production? A dev-only dep (e.g. esbuild, prettier) has no + production attack surface — treat as low urgency. + +2. **Pin or upgrade.** If a non-breaking upgrade is available, update + `frontend/package.json` and regenerate the lockfile. Open a PR. + +3. **Override if justified.** If the advisory does not apply (dev-only dep, no + exploitable path), add an `npm audit` override in `package.json`: + ```json + "overrides": { "esbuild": ">=0.25.4" } + ``` + Document the rationale in the PR body. See #817 for the reference decision tree. + +4. **Close the tracking issue** once the advisory is resolved or overridden and the + nightly job runs clean (verify via the `✅ npm audit clean` heartbeat in the job + summary).