docs(vuln): handoff for automated CVE visibility
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
4ca0ebfac6
commit
e1adc3963c
1 file changed
+84
@@ -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.
|
||||
Reference in new issue
Block a user