# Vulnerability Triage

Trivy Operator writes one `VulnerabilityReport` per container, a few thousand findings on a cluster this size. Almost none warrant action, and the work is in proving that quickly. The operator's configuration and failure modes are in [Trivy Operator](<https://homelab.wlkr.ch/platform/trivy-operator/index.md>). This page is the method; the dispositions are in [Triage 2026-09-20](<https://homelab.wlkr.ch/operations/triage-2026-09-20/index.md>) and, for the Dependency-Track portfolio, [Triage 2026-09-29](<https://homelab.wlkr.ch/operations/triage-2026-09-29/index.md>).

## Pulling a report

```bash
kubectl get vulnerabilityreports -A -o json > vr.json
jq -r '.items[] | .metadata.namespace as $ns
  | .report.artifact.repository as $img
  | .report.vulnerabilities[]?
  | select(.severity=="CRITICAL")
  | [$ns, $img, .vulnerabilityID, .resource, .installedVersion,
     (.fixedVersion // "NO-FIX"), (.packagePath // .target)]
  | @tsv' vr.json | sort -u
```

## Reading it

- **A missing report is not a clean image.** A failed scan writes nothing; the `TrivyContainerNotScanned` alert reconciles running images against scanned ones continuously.
- **An empty `fixedVersion` means no fix is known, not no problem.** Unfixed findings are kept (`ignoreUnfixed` in `payload/platform/trivy-operator/application.yaml`) because a cluster of them in one base image says the base image is the wrong one.
- **The worst findings are usually not the project's code.** The `target` names the binary, and which binary decides whether anything can be done and by whom.
- **Severity is an opinion, and the two sources hold different ones.** [Dependency-Track](<https://homelab-apps.wlkr.ch/dependency-track/>) rates by the advisory's score and Trivy by the distribution's; a finding Trivy has at `UNKNOWN` can be critical there. Use it for which images share a component and for what changed between two tags.
- **Prometheus is not a trend.** It keeps the `retention` in `payload/platform/monitoring/application.yaml`; the daily summary the operator ships to Loki is the history, see [History](<https://homelab.wlkr.ch/platform/trivy-operator/#history>).

Two artifacts recur. A `ConfigAuditReport` has no TTL and goes only when its ReplicaSet is garbage-collected, so superseded revisions keep their findings on the dashboard ([aquasecurity/trivy-operator\#3069](<https://github.com/aquasecurity/trivy-operator/issues/3069>)); when a count looks wrong, check whether the ReplicaSet in the report name still has replicas. `KSV-0125` ("untrusted registry") fires on nearly every container because the check's built-in list is Azure, ECR and GCR, which Trivy Operator does not expose as a parameter; it says nothing about the images.

## Filing policy

Filing goes out under a real name, permanently, and most projects have said what they want:

- **Argo CD** asks in `SECURITY.md` not to raise scanner findings.
- **Home Assistant** (Open Home Foundation) does not accept issues or PRs from autonomous agents.
- **Rook** accepts AI-assisted contributions with disclosure, submitted by a human.
- **Routine Go churn is not a finding.** `grpc` or the stdlib one patch behind is fixed by the next release every project here takes automatically.

"Waiting on release" means the fix is merged upstream and Renovate takes the release when it exists; nothing here speeds it up.

## What is load-bearing

`ConfigAuditReport`, `RbacAssessmentReport` and `InfraAssessmentReport` are fixable here rather than upstream. Two checks are most of the volume: `KSV-0014` (read-only root filesystem) and `KSV-0118` (empty pod-level `securityContext`, whatever the containers set).

| Where | Why it stays |
| --- | --- |
| Cilium, cilium-envoy, kube-vip, the kubeadm static pods, Rook OSDs and mons, node-exporter, Kured | Host network, host PID, privileged and added capabilities are what these do; `pod-security.yaml` enforces `privileged` in those namespaces for this reason |
| `etcd-backup` and `etcd-defrag` CronJobs | Host network, because etcd listens on the node's loopback; root, because the client certificates are `600 root` |
| Velero node agent | Root and capabilities stay: kopia reads every pod volume, whoever owns the files |
| Images that start as root by design (Home Assistant) | `runAsNonRoot` or a read-only root would change how they run; seccomp is set |
| Read-only root on charts that write under `/` at runtime (Grafana sidecars, OpenBao) | The chart or image decides where; guessing costs an outage (\#663) |
| Every critical or high `ClusterRole` | A chart's operator role, or Kubernetes' own `admin`, `edit` and `cluster-admin`; not narrowable without forking a chart — see [Security Posture](<https://homelab.wlkr.ch/architecture/security/#authorization>) |
| The kubelet unit at 644: `KCV-0069` | Accepted: it ships in the signed, read-only sysext and holds no secret. The kubelet config, the CA certificate and the CNI directory (`KCV-0077`, `KCV-0075`, `KCV-0056`) are set to the CIS modes by the `tmpfiles` rule in `ansible/templates/butane_node_config.yaml.j2`, because kubeadm rewrites them at 644 on every `init`, `join` and `upgrade`; the dispositions are in [Triage 2026-09-20](<https://homelab.wlkr.ch/operations/triage-2026-09-20/#infra-the-nodes>) |
