docs(vuln): handoff for automated CVE visibility
gate / gate (pull_request) Failing after 54s
gate / conformance (pull_request) Skipped

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
jakman85andClaude Opus 5.5 committed 2026-11-09 12:40:15 -06:00
1 parent 4ca0ebfac6
commit e1adc3963c
1 file changed
+84
+84
View File
@@ -0,0 +1,84 @@
---
date: 2026-11-09
branch: feat/vuln-visibility
title: Automated CVE visibility for production (R-VULN-01..09, ADR-0027)
---
## What changed
Implements `docs/17-vulnerability-visibility.md` from spec PR Home-Network/home-network-k3s#4
(commit `0fb44bb`, open, not merged). `ci/SPEC_PIN` is deliberately not bumped: the merge SHA does
not exist yet, and open PR #160 owns the pin backlog.
- `platform/vuln-scanner/`: stdlib Python scanner in a ConfigMap on a digest-pinned
`python:3.14.9-alpine`, Trivy 0.76.1 by digest copied in by an initContainer, a per-app
oauth2-proxy sidecar for `cve.${BASE_DOMAIN}`, read-only ClusterRole, RWO Longhorn PVC for the
report and state, size-limited emptyDir for the DB and scan cache, pinned Service ClusterIP
`10.43.210.10` as the bot's DNS fallback, five alerts with promtool tests and a runbook.
- `clusters/base/vuln-scanner.yaml`: its own Flux tier, `wait: false`, `dependsOn: platform-config`,
nothing depends on it.
- `.gitea/workflows/cve-triage.yaml` and `scripts/cve_triage.py`: the bot, cron `15 7 * * *`,
counts-only `CVE Dashboard` issue with its own `cve-dashboard` label, approve-row ticks only.
- `renovate.json5`: Trivy on the blast-radius list, 7-day minimum release age, and everything under
`platform/vuln-scanner/**` non-automerging.
- `conformance/`: R-VULN-01..09 in the registry; L0 checks and L1 fixture tests (scanner, page,
bot) wired by scope, L2/L3 authored as scaffolds.
- PiHole LAN names and the Homepage link gained the CVE page.
### Decisions a future reader will want
- Page is server-rendered with **no JavaScript**, not inline JS: the CSP has no `script-src`,
escaping is in one place, and the L1 test exercises the real rendered page, which a JS page could
not do without a browser engine. Filters, sort and pagination are URL parameters.
- Three pod ports, not two. Prometheus gets 9090 only, so it cannot read `report.json`.
- A killed image (OOM, eviction) is recorded as failed from an in-flight marker and sits out full
runs for 20h, so it cannot wedge the scan loop.
- Private registries are named in `VULN_PRIVATE_REGISTRIES` and short-circuit to
`credential-required` before Trivy runs (see escalation 1).
- Bot's job container is the repo's toolbox image by digest: `actions/checkout` needs node and the
pull is already proven by the merge gate.
### Spec escalations
1. Coverage state `credential-required` is defined as "the registry refused anonymous access", but
the egress contract excludes LAN ranges, so the scanner cannot reach the private registry to be
refused. Implemented by naming private registries in configuration. The spec should define the
state by configuration or admit the registry to egress.
2. The egress contract lists only the Kubernetes API, DNS and public TCP/443, but the SSO sidecar
must reach Keycloak. Egress additionally allows the Gateway on 443 and Keycloak. The spec should
name this path.
## What I verified
- Toolbox gate on the integrated tree: **rc 3 (UNDETERMINED, not green)**. Stages that did not run:
`spec-immutability` and `gitea-settings`; `conformance` (L2/L3) logged SKIPPED, no cluster.
`approval-policy`, `decisions-log`, `lint`, `l0`, `render-golden`, `l1` reported green.
`approval-policy` is N/A offline and proves nothing locally.
- All `vuln-*` L0 and L1 checks pass in the toolbox, each with a mutated-fixture non-vacuity case.
- `renovate-config-validator` from `ghcr.io/renovatebot/renovate:44.167.0`, the tag `renovate.yaml`
runs: validated successfully. This is a read-only container run of a public image.
- Trivy 0.76.1 digest: registry `created` 2026-10-21, 19 days old at authoring; cosign keyless
verification passed (cosign 3.1.3); `cp` and the CA bundle confirmed from a layer listing.
- ClusterIP `10.43.210.10` confirmed unused against the live Service list, read-only.
- An independent reviewer checked the diff against docs/17 and found no anonymous leak path, no way
for an unscanned image to read as clean, no way for the bot to tick a `rebase-branch` row, and no
platform stall path. One BLOCKER and five other code findings were fixed with tests.
## What I did NOT verify
Nothing here has been deployed. No runtime behaviour has been observed.
- **R-VULN-04 is NOT delivered until the operator adds the Keycloak client.** It needs a new key in
`platform/keycloak/keycloak-realm-clients.sops.yaml`, which needs the age private key, and a
one-time `kcadm` against the live realm. Exact steps: `platform/vuln-scanner/README.md`, "Operator
action required". Until then login fails closed with `invalid_client`. Those steps are UNVERIFIED.
- A real Trivy run: every scanner test uses a recorded fixture and a fake runner.
- The real Kubernetes API, real browser rendering, OIDC login and the `platform-admins` restriction.
- Whether in-cluster DNS resolves from a DinD job container, and whether the pinned ClusterIP
fallback is reachable from one. The workflow has never run; Gitea behaviour is tested against a
fake only.
- The `## Pending Approval` row format is from Renovate's source as recalled, because issue #91 has
no live approve row today. The `approve-branch` marker itself is documented.
- L2 and L3 are scaffolds and do not run in CI.
- Renovate `minimumReleaseAge` behaviour on a real update, as opposed to the config validating.
- Cgroup v2 memory behaviour under the scanner's limits is inferred, not measured.