DevOps: Renovate runner + nightly npm audit early-warning (#818) (#821)
Some checks failed
CI / OCR Service Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has started running
CI / Backend Unit Tests (push) Has been cancelled
CI / fail2ban Regex (push) Has been cancelled
CI / Semgrep Security Scan (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
Some checks failed
CI / OCR Service Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has started running
CI / Backend Unit Tests (push) Has been cancelled
CI / fail2ban Regex (push) Has been cancelled
CI / Semgrep Security Scan (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
## 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
This commit was merged in pull request #821.
This commit is contained in:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user