diff --git a/docs/adr/041-renovate-runner-setup.md b/docs/adr/041-renovate-runner-setup.md new file mode 100644 index 00000000..41a17fcd --- /dev/null +++ b/docs/adr/041-renovate-runner-setup.md @@ -0,0 +1,123 @@ +# ADR-041 — Renovate runner stand-up: two-token model, OSV surfacing, digest pinning + +**Date:** 2026-06-13 +**Status:** Accepted +**Issue:** [#818](https://git.raddatz.cloud/marcel/familienarchiv/issues/818) + +--- + +## Context + +Issue #817 (esbuild/cookie advisory) revealed that `main` had no early-warning +mechanism for newly-published advisories. An advisory landed against already-pinned +versions, turned the `npm audit --audit-level=high --omit=dev` gate red on `main`, +and then ambushed the next unrelated PR (#774). The author who hit it did not cause +it and had no warning. + +`renovate.json` existed but `renovatebot` had never actually run: there was no +`.gitea/workflows/renovate.yml` and zero Renovate-authored PRs in the repo's entire +history. The three `packageRules` (bucket4j / tiptap / privileged-digest) were +silently inert. + +This ADR records the **negative space** — why specific design choices were made, +so future maintainers do not "tidy up" toward a worse outcome. + +--- + +## Decision + +### Why there is no auto-provided `GITEA_TOKEN` + +Self-hosted Gitea runners do not auto-inject a `GITEA_TOKEN` equivalent. +`docs/infrastructure/ci-gitea.md` (and its current line ~251) explicitly states the +token "must be created manually." No existing workflow in this repo references +`GITEA_TOKEN` for API calls — only for container registry auth (`docker login`). +Both `RENOVATE_TOKEN` and `NIGHTLY_AUDIT_TOKEN` must be manually provisioned as +Gitea secrets by a repository admin. + +### Why two tokens, not one + +The two jobs have different blast radii on token compromise: + +| Token | Scopes | Used by | +|-------|--------|---------| +| `RENOVATE_TOKEN` | `contents` + `pull_request` + `issues` | Renovate — must read/write files and open PRs | +| `NIGHTLY_AUDIT_TOKEN` | `issues` only | Nightly audit — only needs to file a tracking issue | + +The nightly job's token appears in step `env:` and is passed to `curl -H`. A leak via +runner logs, process arguments, or a misconfigured step would expose the token. +An `issues`-only token cannot push branches, open PRs, or read repository contents — +the leaked token's blast radius is limited to creating/editing issues. + +A single broad token would give any leak path full `contents` + `pull_request` write +access to the repository. That risk is asymmetric with the upside (one fewer secret). + +Both tokens belong to one dedicated bot account (consistent authorship; one identity +to audit and rotate). **Branch protection on `main` must forbid the bot pushing +directly**, because a `contents`-scoped token can push to any unprotected branch. + +### Why the Renovate action is digest-pinned + +`renovatebot/github-action` executes with the `RENOVATE_TOKEN` in scope. That token +carries `contents` + `pull_request` + `issues` — enough to read files, open PRs, and +write issues. An unpinned `@v40` tag can be re-pointed by the upstream maintainer +(or a compromised maintainer account) at any time. A pinned digest (`@`) cannot +be silently modified; the SHA is immutable. This is the same threat model applied to +all privileged CI steps in this repo (see the `matchFileNames` rule in `renovate.json` +for `.gitea/workflows/**`). + +Renovate itself will open a PR to bump the digest when a new release ships, which is +the intended update path. + +### Why `osvVulnerabilityAlerts` is the load-bearing detector on Gitea + +Renovate's `vulnerabilityAlerts` config key triggers off a *platform* vulnerability +graph. GitHub exposes the GitHub Advisory Database via its API; **Gitea does not +expose an equivalent vulnerability graph**. On self-hosted Gitea, `vulnerabilityAlerts` +is effectively a label carrier — it attaches the configured labels to PRs that +`osvVulnerabilityAlerts` already detected, but it is not an independent detector. + +`osvVulnerabilityAlerts: true` is the load-bearing flag: Renovate queries +[OSV.dev](https://osv.dev) directly (platform-agnostic). The runner host must be able +to reach OSV.dev over HTTPS — if egress is filtered, allow `osv.dev:443` or the flag +silently no-ops. + +### Why the root `schedule` does not mute security PRs + +`"schedule": ["before 6am on monday"]` in `renovate.json` batches **routine** dependency +updates (version bumps outside any security context) to a weekly window. This reduces +noise from routine update PRs while still allowing review before merge. + +**Security and vulnerability PRs bypass the schedule by design** — Renovate raises +them immediately regardless of the schedule window. A future "tidy-up" that removes +or widens the schedule cannot mute vulnerability alerts; this is worth stating +explicitly to prevent that misunderstanding. + +### Why `lockFileMaintenance` has no `automerge` + +`lockFileMaintenance` refreshes transitive pins weekly so the dependency tree drifts +into fewer advisories over time. It is explicitly set without `automerge: true` because +a weekly transitive pin refresh can silently break the build if a transitive dep +introduces a breaking change. These PRs are small and should be reviewed. + +### Why there is no entry in `l2-containers.puml` + +`docs/architecture/c4/l2-containers.puml` documents long-lived infrastructure +containers (services that run continuously). Renovate is a scheduled CI job that runs +on a Gitea Actions runner and exits — it is not a long-lived container. Adding it to +the container diagram would misrepresent the architecture. This omission is deliberate, +not an oversight. + +--- + +## Consequences + +- Newly-published advisories against our frontend dependencies are surfaced within + one day (daily Renovate cron) rather than at the next contributor PR. +- A nightly `npm audit` job provides an independent signal for dev-dependency advisories + that Renovate may not cover via OSV. +- Two secrets (`RENOVATE_TOKEN`, `NIGHTLY_AUDIT_TOKEN`) must be manually provisioned + and rotated annually (or on suspected compromise). See + `docs/infrastructure/ci-gitea.md` for the runbook. +- The bot account must be kept active and branch protection on `main` must forbid + it pushing directly. These are operational prerequisites, not code invariants.