diff --git a/GLOSSARY.md b/GLOSSARY.md index 732c441..66446fe 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -67,6 +67,11 @@ two things (e.g. "VM"), this file disambiguates. - **DR drill** — a tested restore of resources + volumes proving R-STATE-03. ## Delivery & lifecycle +- **Coverage state** — whether a target was actually evaluated for vulnerabilities (`scanned`, + `pending`, `stale`, `failed`, `credential-required`, `by-tag`, `not-covered`). Zero findings means + something only in `scanned`. See [`docs/17-vulnerability-visibility.md`](docs/17-vulnerability-visibility.md). +- **CVE triage bot** — the scheduled CI workflow that summarises the vulnerability report and asks + Renovate to open pending updates; it never authors a version change. - **GitOps** — desired state lives in git and is reconciled into the cluster automatically (pull-based). Engine: **Flux CD**. - **Drift correction** — Flux reverting live changes back to git-declared state (R-GITOPS-03). diff --git a/README.md b/README.md index bbe5890..3c54662 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,8 @@ home-network-k3s/ │ ├── 13-service-catalog.md │ ├── 14-testing-and-conformance.md │ ├── 15-identity-and-access.md -│ └── 16-known-gaps.md +│ ├── 16-known-gaps.md +│ └── 17-vulnerability-visibility.md ├── decisions/ ← ADRs (one decision per file) + index/template ├── roadmap/ ← phases.md, workstreams.md └── agents/ ← roles, coordination protocol, DoD, task board diff --git a/decisions/README.md b/decisions/README.md index 5ffa368..e6f3927 100644 --- a/decisions/README.md +++ b/decisions/README.md @@ -76,3 +76,4 @@ pattern choices. | [0024](adr-0024-delivery-authority-ci-gate.md) | Delivery authority: the CI gate is the approver; sensitive-path review; attributed admin override | Accepted | R-TEST-03, R-TEST-06, R-GITOPS-01, R-SEC-02 | | [0025](adr-0025-in-cluster-ai-program-manager.md) | In-cluster AI Program Manager (pm-agent) | Accepted | R-GITOPS-01, R-TEST-03, R-TEST-06, R-SEC-02 | | [0026](adr-0026-fault-injection-operator-run.md) | Fault injection is operator-run, never automated | Accepted | R-TEST-03, R-TEST-05, R-ENV-03 | +| [0027](adr-0027-vulnerability-visibility.md) | Vulnerability visibility: scheduled digest scanning with a Renovate-shaped bot | Accepted | R-VULN-01..09 | diff --git a/decisions/adr-0027-vulnerability-visibility.md b/decisions/adr-0027-vulnerability-visibility.md new file mode 100644 index 0000000..e431e57 --- /dev/null +++ b/decisions/adr-0027-vulnerability-visibility.md @@ -0,0 +1,67 @@ +# ADR-0027 — Vulnerability visibility: scheduled digest scanning with a Renovate-shaped bot +- Status: Accepted +- Date: 2026-11-09 +- Serves: R-VULN-01..09, R-OBS-02, R-OBS-03, R-C2-02, R-UPD-02, R-SEC-02, R-IDP-06 +- Supersedes: — +- Accepted by: operator merge of this change; effective on merge. + +## Context +The operator has no view of the vulnerabilities in what production runs. Renovate moves versions +forward on a schedule, but it knows nothing about CVEs, and nothing in the platform assesses a running +image. Measured on 2026-11-09: + +- 259 containers run from 98 unique digests, across eight or more registries, with no scanner present. +- Prometheus holds 427k head series, using about 2.75 GiB of its 3 GiB limit. +- etcd and Longhorn share a partition on every node (accepted risk, [docs/02](../docs/02-environments-and-provisioning.md)), + and etcd's own metrics are not scraped, so the headroom for added etcd writes cannot be measured. +- CI holds no cluster credential by decision, and has two runner slots that also serve the merge gate. +- The implementation repository is readable without authentication. +- Trivy's release pipeline was compromised in March 2026 (CVE-2026-33634). + +## Decision +**A small scheduled scanner runs in the cluster, built on the Trivy CLI, and a Renovate-shaped CI +bot turns its results into action.** The full contract is [docs/17](../docs/17-vulnerability-visibility.md). + +- **Scanner.** Takes its inventory from the Kubernetes API, scans each unique digest with Trivy, and + stores one versioned report on a small dedicated volume, outside etcd and Prometheus. +- **Page.** Served from that report behind SSO and restricted to `platform-admins`. +- **Metrics.** Aggregates only. +- **Bot.** A scheduled Gitea Actions workflow using the existing CI-scoped Renovate credential. It + maintains a counts-only dashboard issue and requests Renovate PRs through Renovate's own + dashboard approval mechanism. +- **Renovate stays the sole author of version changes.** No second bot edits a tag. +- **Delivery tooling, not a catalog service.** Its cluster-wide read role is something a + ServiceDefinition cannot express, so it is hand-authored under `platform/` with its own non-gating + Flux tier, as in ADR-0025. + +## Alternatives considered +- **Trivy Operator.** The obvious choice, and rejected on measurement. It stores one report per + workload as a CRD, adding write load to an etcd we cannot currently measure and which shares its + disk with Longhorn; the largest images produce the largest reports, so size-limit failures would + hide exactly the worst offenders. It needs broad cluster permissions and job-creation rights. It + would also still need a bespoke aggregator, because the per-CVE page cannot be fed from Prometheus. +- **Grafana table over per-CVE Prometheus metrics.** Needs no new page, but adds tens of thousands of + long-labelled series to a Prometheus at about 92% of its memory limit. Raising the limit treats the + symptom; a CVE inventory is not time-series data. +- **Scanning from Gitea Actions.** Closest to Renovate literally, but CI has no view of what is + running, and a long scan would occupy one of the two runner slots that gate merges. +- **A third-party Trivy dashboard.** Assumes the operator's CRDs, and adds a further small-maintainer + supply-chain dependency to a sensitive surface. +- **An in-cluster bot with a Gitea token.** Puts a git-writing credential in the cluster, which + ADR-0023 rejects. +- **A CVE dashboard issue listing the findings.** The most Renovate-like, and rejected because the + repository is publicly readable and the list would be an attack map. +- **A bot that authors bump PRs itself.** A package's fixed version does not identify the image tag + containing it, and two version authors would contend. Rejected in favour of driving Renovate. + +## Consequences +- Scanning and its output are hands-off, and "not scanned" is visible rather than silent. +- The bot reuses the Renovate token, so both act as `renovate-bot`. This avoids a new credential; + splitting them later changes one workflow secret. +- The platform carries a small custom component that it must maintain and test. The report schema + isolates the scanner, so Trivy can be replaced without touching the page or the bot. +- Findings for private-registry images, VMs and nodes remain unassessed, and are shown as such + ([known gaps](../docs/16-known-gaps.md)). +- The critical-fix alert will fire on first deployment. That is the backlog becoming visible. +- A tick can only open a Renovate PR. Remediation of majors and blast-radius components still waits + for human review. diff --git a/docs/00-vision-and-scope.md b/docs/00-vision-and-scope.md index edcef51..f32f9e3 100644 --- a/docs/00-vision-and-scope.md +++ b/docs/00-vision-and-scope.md @@ -149,6 +149,28 @@ mandatory; `SHOULD` is strongly preferred and requires an ADR to deviate. - **R-SEC-04** — The Kubernetes API MUST NOT be reachable from the public internet by any path (no tunnel route, no edge forward, no public DNS record); it is LAN/VPN-only (ADR-0016). +### Vulnerability visibility — `R-VULN-*` +- **R-VULN-01** — The platform MUST maintain an inventory of every container image running in + production, by digest, mapped to the namespaces, workloads and containers using it, taken from the + running cluster and not from git. +- **R-VULN-02** — Every running image MUST be evaluated against a current vulnerability database at + least every 24 hours, and a newly running image within 2 hours. +- **R-VULN-03** — Any target not successfully evaluated MUST be visibly reported as such with its + reason. Absence of data MUST NOT be presented as absence of vulnerabilities. +- **R-VULN-04** — An authenticated page MUST list every known CVE and, for each, the images, + containers and workloads it affects, and MUST be searchable and filterable. +- **R-VULN-05** — Inventory, scanning, reporting and triage MUST run on a schedule with **no human + and no AI action**, and MUST fail loudly when they cannot run. +- **R-VULN-06** — Scan failure, staleness, and unaccepted CRITICAL vulnerabilities with an available + fix MUST be reported by **email**, as grouped alerts. +- **R-VULN-07** — A scheduled bot MUST maintain a single dashboard issue and request remediation + through Renovate. Renovate MUST remain the only author of version changes, and CVE detail MUST NOT + be published to any surface readable without authentication. +- **R-VULN-08** — Accepted risk MUST be declared in git, through the merge gate, with a reason and a + mandatory expiry, and MUST revert to open automatically when it expires. +- **R-VULN-09** — The scanner MUST be digest-pinned, MUST NOT auto-merge its own updates, MUST NOT + read Secrets or hold Kubernetes write access, and MUST hold no git credential. + ### Identity & access (SSO) — `R-IDP-*` - **R-IDP-01** — The platform MUST provide a central Identity Provider (SSO) as the single user directory and login for platform services. diff --git a/docs/10-observability-and-alerting.md b/docs/10-observability-and-alerting.md index e94ecb3..0157032 100644 --- a/docs/10-observability-and-alerting.md +++ b/docs/10-observability-and-alerting.md @@ -62,6 +62,9 @@ Grafana dashboards, surfaced in the C2 UI ([12](12-command-center-ui.md)): - **Network**: Hubble flows, egress/VPN health, denied-flow trends. - **Per-service**: one row/dashboard per catalog service with its declared SLIs. +The CVE listing is deliberately **not** a Grafana dashboard: per-CVE data does not belong in +Prometheus. See [17](17-vulnerability-visibility.md), which also defines the vulnerability alerts. + ## Observability as onboarding output The ServiceDefinition ([13](13-service-catalog.md)) declares observability expectations; the diff --git a/docs/11-automated-updates.md b/docs/11-automated-updates.md index 66dd7aa..5f64861 100644 --- a/docs/11-automated-updates.md +++ b/docs/11-automated-updates.md @@ -78,6 +78,12 @@ each declared ceiling and carries any per-service cadence. - **Pin escape hatch**: an operator can pin a service via git if an upstream release misbehaves — the one sanctioned human touch, and it *stops* automation rather than performing an update. +## Vulnerability-driven requests + +The CVE triage bot ([17](17-vulnerability-visibility.md)) can ask Renovate to open a pending update +for an image with an unaccepted CRITICAL that has a fix, using Renovate's own dashboard approval. +Renovate remains the only author of version changes, and every merge rule above still applies. + ## Interaction with default-deny & egress Updates change image versions, not network posture. A VPN-class service stays VPN-class across updates; policies are reconciled from the (unchanged) ServiceDefinition. diff --git a/docs/12-command-center-ui.md b/docs/12-command-center-ui.md index cba9eba..57debaa 100644 --- a/docs/12-command-center-ui.md +++ b/docs/12-command-center-ui.md @@ -37,6 +37,8 @@ root aggregates and links them. Status of this composition is tracked in - **Platform health**: node status, Longhorn capacity/health, Flux reconciliation, cert expiry, backup status, VPN tunnel state — sourced from the observability stack ([10](10-observability-and-alerting.md)). - **Alerts**: current firing alerts (mirrors what email delivers). +- **Vulnerabilities**: a link to the CVE page, restricted to `platform-admins` + ([17](17-vulnerability-visibility.md)). ## Actions (safe, audited) diff --git a/docs/14-testing-and-conformance.md b/docs/14-testing-and-conformance.md index bb87217..a170681 100644 --- a/docs/14-testing-and-conformance.md +++ b/docs/14-testing-and-conformance.md @@ -103,6 +103,10 @@ Each maps to requirement IDs; phrased as Given/When/Then intent. an email alert is delivered and the dashboard reflects it. - **GitOps drift (R-GITOPS-03, L3):** *Given* a reconciled resource, *When* it is mutated live, *Then* Flux reverts it to the git-declared state. +- **Vulnerability visibility (R-VULN-*, L0/L1/L2/L3):** *Given* the running inventory, *Then* every + image is either evaluated within its window or visibly reported as not evaluated, and each CVE is + listed against its images, containers and workloads. Full list in + [17](17-vulnerability-visibility.md). - **Onboarding completeness (WS-APPS):** *Given* only a merged ServiceDefinition, *Then* the service is fully wired (compute/storage/policy/egress/TLS/DNS/monitoring/update) with no manual step — asserted by the union of the relevant tests above against that service. diff --git a/docs/16-known-gaps.md b/docs/16-known-gaps.md index ab07599..9e7fdd0 100644 --- a/docs/16-known-gaps.md +++ b/docs/16-known-gaps.md @@ -79,6 +79,7 @@ This cannot be settled by reading either repository. | R-C2-02 | Grafana dashboards are specified as embedded in the C2, but Homepage only links to Grafana. | `platform/homepage/configmap.yaml` | | R-C2-02 | Flux status is specified via Headlamp's Flux view, the Flux UI or a Homepage widget, but Headlamp has no Flux plugin configured and Homepage has no widget. | `platform/headlamp/helmrelease.yaml`, `platform/homepage/configmap.yaml` | | R-C2-02 | C2 actions are specified to include VM start/stop, Flux reconcile and alert acknowledgement, but only native Headlamp workload actions exist, with no plugins for the others. | `platform/headlamp/helmrelease.yaml` | +| R-VULN-01, R-VULN-02 | KubeVirt guests, node OS packages and the k3s binary are inventoried but not vulnerability-assessed; private-registry images need a read-only credential the operator has yet to mint. All report as not evaluated (docs/17). | not yet implemented | ## Service catalog diff --git a/docs/17-vulnerability-visibility.md b/docs/17-vulnerability-visibility.md new file mode 100644 index 0000000..01804f2 --- /dev/null +++ b/docs/17-vulnerability-visibility.md @@ -0,0 +1,290 @@ +# 17 — Vulnerability Visibility + +Serves: **R-VULN-01..09**, surfaces **R-OBS-02/03**, **R-C2-02**, **R-UPD-01/02**, **R-SEC-02**, +**R-IDP-06**. Decision record: [ADR-0027](../decisions/adr-0027-vulnerability-visibility.md). + +## Purpose + +The operator must be able to answer three questions at any time, with no manual step: + +1. **What is running in production?** Every container image, by digest, and the workloads using it. +2. **What is it vulnerable to?** Every known CVE in those images, and where each one lands. +3. **Can I trust the answer?** How fresh it is, and exactly what was *not* examined. + +The third question is as important as the first two. A vulnerability list that silently omits an +image reads the same as a clean bill of health. Everything below treats "not scanned" as a +first-class state that is always visible, and never as zero findings. + +## Shape of the solution + +The workflow is deliberately the same shape as Renovate ([11](11-automated-updates.md)): scheduled, +self-hosted, run to completion, machine-authored output, a single dashboard issue, and a loud failure +when it cannot run. + +``` + Kubernetes API (read-only) + │ running pods → digests → workloads + ▼ + ┌──────────────────────────── namespace: vuln-scanner ─────────────────────────┐ + │ Scanner (scheduled, one image at a time, Trivy) │ + │ └─► Report store (small PVC) ─► report.json (schema below) │ + │ ├─► CVE page cve., SSO, platform-admins │ + │ ├─► /metrics aggregates only → Prometheus → email │ + │ └─► internal API ◄──────────────┐ │ + └─────────────────────────────────────────────┼────────────────────────────────┘ + │ read report; post heartbeat + remediation state + ┌────────────────┴───────────────┐ + │ Gitea Actions: CVE triage bot │ scheduled, before Renovate + └────────────────┬───────────────┘ + │ scoped CI token + ▼ + CVE Dashboard issue (counts only) · approval ticks on Renovate's dashboard + │ + ▼ + Renovate opens the PR → merge gate → Flux (the existing update path) +``` + +The split follows two standing boundaries: **CI holds no cluster credential**, and **the cluster +holds no git credential** ([ADR-0023](../decisions/adr-0023-automated-updates-renovate.md)). The +scanner sees the cluster and never writes to git. The bot writes to Gitea and never touches the +Kubernetes API. + +## Inventory (R-VULN-01) + +The inventory is taken from the **Kubernetes API**, never from git. Git states intent; the question +is what is actually running. + +- It covers containers, init containers and ephemeral containers of every pod in every namespace. +- The identity of a running image is the **digest reported in the container status**. A mutable tag + is not an identity. +- Each pod resolves through its owner chain to the **top-level workload** the operator recognises + (Deployment, StatefulSet, DaemonSet, CronJob, or a bare Pod), not the intermediate ReplicaSet or Job. +- **CronJobs with no pod currently running are still inventoried**, from their pod template. Their + images are recorded by reference and marked `by-tag`, because no digest exists until they run. +- KubeVirt VMs and nodes are inventoried as **non-container targets** and reported as not covered + (see [scope](#scope)). + +## Scanning (R-VULN-02, R-VULN-05) + +| Property | Contract | +|---|---| +| Unit of work | One scan per **unique digest**, mapped back to all its workloads. Measured 2026-11-09: 259 containers resolve to 98 digests. | +| Full re-evaluation | Every running digest is evaluated against a current database **at least every 24 hours**, so a CVE published against an unchanged image is found without any deployment. | +| New digests | A digest not yet seen is scanned **within 2 hours** of first appearing. | +| Database freshness | A scan uses a vulnerability database no older than **48 hours**. Older than that, the run is reported as degraded, not as a success. | +| Severity scope | All severities are recorded, including those with no fix. Filtering is a page concern. | +| Failure isolation | One image failing (pull error, timeout, out of memory) records that image as `failed` with its reason and the run continues. | +| Load | Sequential, one image at a time, with CPU and memory limits. Scan caches MUST NOT live on a replicated Longhorn volume or in etcd, because etcd and Longhorn share a partition on every node ([02](02-environments-and-provisioning.md)). | +| Trigger | Schedule only. No human or AI action starts, resumes or completes a cycle. | + +Findings persist a **first-seen** timestamp across runs, keyed by CVE and digest, so age is +measurable. A finding that disappears because an image was updated leaves the report at the next +inventory; one that disappears because a scan *failed* does not. + +## Coverage (R-VULN-03) + +Every inventoried target has exactly one state: + +| State | Meaning | +|---|---| +| `scanned` | Evaluated within the 24-hour window. Zero findings is meaningful only in this state. | +| `pending` | Discovered and not yet scanned, within the 2-hour window. | +| `stale` | Last successful scan is older than the 24-hour window. Prior findings are retained and labelled stale. | +| `failed` | The most recent attempt failed. The reason is shown. Prior findings are retained. | +| `credential-required` | The image is on a registry declared as needing a credential the scanner does not hold. Shown with the registry. A declared list is used because the private registry sits on the LAN, which the scanner's egress excludes, so it would otherwise surface as a network failure. | +| `by-tag` | Scanned by mutable reference, because no running digest exists. | +| `not-covered` | A target class this scanner cannot examine: VMs and nodes. | + +The page and the report MUST NOT present a total or a "clean" indication that silently excludes any +target. The header of the page always states how many targets are in each state. + +## The report contract + +`report.json` is the one interface between the scanner, the page and the bot. It is versioned and +the implementation validates it against a schema. + +```yaml +schemaVersion: 1 +generatedAt: +database: { updatedAt: , version: } +run: { startedAt: , finishedAt: , status: complete | degraded } +targets: + - id: + type: image | vm | node + image: + digest: + state: + reason: + lastScannedAt: + workloads: + - { namespace: , kind: , name: , containers: [] } +findings: + - id: + targetId: + severity: CRITICAL | HIGH | MEDIUM | LOW | UNKNOWN + cvss: + package: + installedVersion: + fixedVersion: + title: + url: + firstSeenAt: + status: open | fix-available | accepted + acceptance: { reason: , expires: } # present only when accepted +remediation: # written by the triage bot, may be empty + - { image: , state: , url: , updatedAt: } +``` + +Scanner-derived text (titles, package names, advisory URLs) originates upstream and is **untrusted**. +The page MUST escape it and serve a restrictive Content-Security-Policy, and only `http(s)` URLs may +become links. + +## The CVE page (R-VULN-04) + +A page at `cve.`, linked from the C2 landing page. + +**Header, always visible:** report age, database age, per-state target counts, and open findings by +severity. If the report is stale or degraded, the page says so prominently and does not show a +normal state. + +**CVEs view (default):** one row per CVE, with id (linking to the advisory), severity, CVSS, +package, fixed version, first seen, status, and the counts of affected images and workloads. Each +row expands to list **every image, container and workload** it touches, with namespace. + +**Workloads view:** one row per workload, with its containers, images and per-severity counts, +expanding to its CVEs. + +**Inventory view:** every target and its coverage state, *including those with zero findings*. This +is the answer to "what am I running". + +**Interaction:** free-text search, and filters for severity, namespace, workload, status and "fix +available", with sortable columns. Filter state is in the URL so a view can be linked. The +default view excludes nothing. + +**Remediation column:** for images with a fix available, one of `pr-open` (with link), +`approval-requested`, `no-update-identified` or `not-tracked`, taken from the bot. + +**Access:** internal exposure only, behind SSO, restricted to `platform-admins` (R-IDP-06). A list of +what is exploitable where is sensitive, so this is not a "logged-in user" surface. The page is +read-only and offers no actions. + +## Alerting (R-VULN-06) + +Alerts are delivered through the existing Alertmanager email route. Each alert is **one grouped +message per condition**, never one per CVE. + +| Alert | Fires when | +|---|---| +| Scan stale | No complete run within 26 hours, **or the metric is absent** | +| Database stale | Vulnerability database older than 48 hours | +| Coverage gap | Any target in `failed` for more than 24 hours | +| Critical fix available | Any unaccepted CRITICAL with a fix, first seen more than 72 hours ago | +| Triage bot silent | No bot heartbeat for 26 hours, **or the metric is absent** | + +The 72-hour grace gives Renovate's daily cycle time to act, so email marks the cases automation has +not resolved. The critical-fix alert is expected to fire on first deployment. + +Prometheus receives **aggregates only**: counts by severity and fixability, per-state target counts, +timestamps. It MUST NOT receive a per-CVE series. Measured 2026-11-09: Prometheus holds 427k head +series at about 2.75 GiB of a 3 GiB limit. + +## The triage bot (R-VULN-05, R-VULN-07) + +A scheduled Gitea Actions workflow that follows the conventions of the Renovate workflow: it fails +hard on a missing credential rather than skipping, has a `workflow_dispatch` dry run, never runs +concurrently with itself, and records what it did in the job summary. It runs **before** the daily +Renovate run so that approvals take effect the same morning. + +Each run it: + +1. Reads `report.json` over the internal API. +2. Creates or updates a single **CVE Dashboard** issue on the implementation repository. +3. Reads Renovate's Dependency Dashboard and open PRs, and derives each image's remediation state. +4. Ticks the **approval** checkbox on Renovate's dashboard for a pending update whose package matches + an image with an unaccepted CRITICAL that has a fix. +5. Posts its heartbeat and the remediation states back to the scanner. + +**Renovate remains the only author of version changes** (R-UPD-02, ADR-0023). The bot never edits a +version, opens a bump PR or merges anything. A tick only asks Renovate to *open* a PR; majors and +blast-radius components still wait for human review, exactly as today. + +**The dashboard issue carries aggregates only:** total findings by severity, per-state coverage, +timestamps and a link to the page. It MUST NOT name a CVE, package or image-to-workload relationship, +because the repository is readable without authentication (measured 2026-11-09). It MUST NOT carry any +label that the in-cluster Program Manager acts on ([ADR-0025](../decisions/adr-0025-in-cluster-ai-program-manager.md)). + +The internal API accepts reads and a single bounded write (heartbeat plus remediation states, schema +validated). It is reachable only from the CI runner namespace, by network policy. + +## Risk acceptance (R-VULN-08) + +A CVE the operator has decided to live with is declared **in git**, through the merge gate, with +an expiry: + +```yaml +- id: CVE-2026-00000 + image: registry/repository # exact match; omit only for a platform-wide acceptance + reason: not reachable — feature disabled + expires: 2027-02-01 # mandatory, at most 180 days out +``` + +An accepted finding stays in the listing, marked `accepted` with its reason and expiry, and stops +counting toward the critical-fix alert. **When the expiry passes it reverts to open automatically.** +An entry with no reason or expiry is rejected by the gate. + +## Security of the scanner itself (R-VULN-09) + +A scanner reads every image and is a supply-chain target. Trivy's release and CI tooling were +compromised in March 2026 ([CVE-2026-33634](https://nvd.nist.gov/vuln/detail/CVE-2026-33634)), so: + +- The scanner binary is pinned **by digest**. It is a Renovate blast-radius component and never + auto-merges, and an update is proposed only after a **minimum release age of 7 days**. +- Trivy 0.69.4, 0.69.5 and 0.69.6 MUST NOT be used. +- Identity: `get/list/watch` on pods and their owner kinds, CronJobs, nodes and VM instances, and + **nothing else**. It MUST NOT read Secrets or hold any write verb on the Kubernetes API. +- It holds no git credential and no registry credential. +- Network: ingress only from the Gateway for the page, from CI for the internal API and from + Prometheus for metrics. Egress only to the Kubernetes API, DNS, the platform Gateway and identity provider (required by the + SSO sidecar), and TCP/443 on the public internet, with cluster and LAN ranges otherwise excluded, + through the direct path. +- It runs unprivileged, with a read-only root filesystem and no host namespaces or mounts. +- The scanner's own images appear in the report like any other. + +## Scope + +**In scope:** every container image running in a pod, on every node, in every namespace, including +the platform's own components. + +**Not in scope for this version, and reported as `not-covered` rather than omitted:** guest OS +packages inside KubeVirt VMs, and node OS packages and the k3s binary. These are updated by the +mechanisms in [11](11-automated-updates.md) but are not vulnerability-assessed here. + +Also out of scope: application dependency scanning of source, misconfiguration and secret scanning, +runtime exploit detection, and blocking deployment based on findings. + +## Acceptance gates + +Each is an automated, requirement-tagged test ([14](14-testing-and-conformance.md)). + +- **L0:** digest-pinned scanner image; no banned version; Renovate rule keeps it out of auto-merge + with the minimum release age; RBAC contains no Secret access and no write verb; no per-CVE metric + label; alerts unit-tested, including the absent-metric cases; the bot workflow fails on a missing + credential; acceptance entries without reason or expiry rejected. +- **L1 (fixtures, no cluster):** inventory resolves ReplicaSet to Deployment and Job to CronJob; + two workloads sharing a digest produce one scan and both mappings; one failed image leaves the run + continuing with that target `failed` and its prior findings retained; an expired acceptance reverts + to open; the rendered page contains every fixture CVE, container and workload; a hostile title is + escaped; the page shows a warning for a stale report; a dashboard issue rendered from a fixture + contains no CVE id or package name; a tick is applied only to an `approve` row for a matching image + with a qualifying finding. +- **L2:** a newly started workload's digest appears in the report within the window; the page requires + a `platform-admins` login; the internal API is unreachable from an unrelated namespace. +- **L3:** breaking registry access produces `failed` targets and fires the coverage alert. + +## Open questions + +- **Private registry coverage.** The platform's own images need a registry credential, so they report + `credential-required`. Closing this requires a read-only registry token, an operator action. +- **Exploitability ranking.** Adding CISA KEV or EPSS data, and weighting by whether a workload is + publicly exposed, would rank the list better than CVSS alone. +- **VM and node assessment.** How to bring `not-covered` targets into scope. diff --git a/roadmap/phases.md b/roadmap/phases.md index 541bb19..3471000 100644 --- a/roadmap/phases.md +++ b/roadmap/phases.md @@ -105,6 +105,17 @@ alerts live. --- +## Phase 8 — Vulnerability Visibility +**Status:** open; may proceed alongside Phase 7. +**Entry:** ADR-0027 accepted. +**Do:** deliver the scanner, report, CVE page, alerts and triage bot in +[`../docs/17-vulnerability-visibility.md`](../docs/17-vulnerability-visibility.md). +**Exit:** every running image is evaluated within its window or visibly reported as not evaluated; +the acceptance gates in docs/17 pass. +**Serves:** R-VULN-01..09. + +--- + ## Dependency flow ```