Files
home-network-k3s-impl/decisions/2026-11-09-vuln-visibility.md
T
jakman85andClaude Opus 5.5 e1adc3963c
gate / gate (pull_request) Successful in 1m5s
gate / conformance (pull_request) Skipped
docs(vuln): handoff for automated CVE visibility
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 12:40:15 -06:00

5.3 KiB

date, branch, title
date branch title
2026-11-09 feat/vuln-visibility 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.