## Summary Closes #818. Sets up the prevention layer so newly-published advisories are caught on a branch we own, not on a contributor's PR. **What changed:** - `renovate.json` — migrated 2 deprecated keys (`matchPackagePatterns` → `matchPackageNames`, `matchPaths` → `matchFileNames`); added `osvVulnerabilityAlerts`, `dependencyDashboard`, `vulnerabilityAlerts` (labels: security + P1-high), weekly routine `schedule`, and `lockFileMaintenance` (no automerge) - `.gitea/workflows/renovate.yml` — **new** daily cron runner (`0 3 * * *`), pinned to `renovatebot/github-action@8217b3fc` (v46.1.15) with `renovate-version: "46.1.15"`, `RENOVATE_TOKEN` secret, Gitea platform/endpoint env vars - `.gitea/workflows/nightly.yml` — added `npm-audit` job (parallel to `deploy-staging`, independent signal): shell self-test, `set +e` audit capture, jq-built deduped issue open/update, `NIGHTLY_AUDIT_TOKEN` via step env only, heartbeat on clean path - `docs/adr/041-renovate-runner-setup.md` — **new** negative-space ADR (no auto GITEA_TOKEN, two-token rationale, OSV-vs-platform, digest-pin threat model, schedule-batches-routine-only, l2-containers omission) - `docs/infrastructure/ci-gitea.md` — two-token model, PAT rotation cadence, OSV-vs-platform, nightly/PR-gate divergence table, runbook for nightly-opened issues - `docs/infrastructure/self-hosted-catalogue.md` — fixed Renovate snippet (daily cron, digest pin, `RENOVATE_TOKEN`, fixed version, no root `automerge: true`) **No `l2-containers.puml` entry** — Renovate is a scheduled CI job, not a long-lived container. Stated here as a decision, not an oversight (ADR-041). ## Manual steps required before the runner is live (not automated) 1. Create a dedicated bot account (e.g. `renovate-bot`) on the Gitea instance 2. Mint `RENOVATE_TOKEN` PAT (scopes: `contents` + `pull_request` + `issues`) → add as Gitea secret 3. Mint `NIGHTLY_AUDIT_TOKEN` PAT (scope: `issues` only) → add as Gitea secret 4. Configure `main` branch protection to forbid the bot pushing directly ## Acceptance criteria status - [x] `renovate.json` deprecated keys migrated; vuln surfacing config enabled - [x] `.gitea/workflows/renovate.yml` exists (digest-pinned, daily cron, fixed version) - [x] `self-hosted-catalogue.md` snippet corrected (4 items) - [x] `nightly.yml` npm-audit job: survives non-zero exit, deduped tracking issue, jq payload, NIGHTLY_AUDIT_TOKEN via env only, heartbeat on clean - [x] ADR-041 records all negative-space decisions - [x] `ci-gitea.md` documents two-token model + runbook - [ ] Phase 0 manual gates: bot account creation, Renovate onboarding PR evidence, Dependency Dashboard screenshot — **requires manual provisioning** - [ ] Dedupe AC verified via `workflow_dispatch` — **requires NIGHTLY_AUDIT_TOKEN secret to be provisioned first** - [ ] `$GITHUB_STEP_SUMMARY` availability on this runner — **verify in first live run** Co-authored-by: Marcel <marcel@familienarchiv> Reviewed-on: #821
6.0 KiB
ADR-041 — Renovate runner stand-up: two-token model, OSV surfacing, digest pinning
Date: 2026-06-13
Status: Accepted
Issue: #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 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 auditjob 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). Seedocs/infrastructure/ci-gitea.mdfor the runbook. - The bot account must be kept active and branch protection on
mainmust forbid it pushing directly. These are operational prerequisites, not code invariants.