docs(adr): add ADR-041 Renovate runner stand-up (two-token, OSV, digest pin)

Records the negative space for #818: why no auto GITEA_TOKEN, why two
tokens not one, why digest-pin on the Renovate action, OSV-vs-platform
distinction on self-hosted Gitea, why the weekly schedule does not mute
security PRs, why lockFileMaintenance has no automerge, and why there is
no l2-containers.puml entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-13 11:25:45 +02:00
parent 5dc1bf6bfb
commit d56a9eb401

View File

@@ -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 (`@<sha>`) 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.