Inhoudsopgave
- Doel
- Vereisten
- Beveiligingsadvies: Trivy supply chain compromise (maart 2026)
- Wat Trivy scant in een container image
- Stap 1: installeer Trivy en voer je eerste scan uit
- Stap 2: filter resultaten en onderdruk geaccepteerde risico's
- Stap 3: blokkeer je CI-pipeline met GitHub Actions
- Stap 4: genereer en attesteer een SBOM
- Stap 5: onderteken images met Cosign
- Stap 6: verifieer image-signatures met Kyverno
- Stap 7: continue in-cluster scanning met de Trivy Operator
- Stap 8: monitor vulnerability reports met Prometheus en Grafana
- Je aanvalsoppervlak verkleinen: image-minimalisatie
- Drie misvattingen over container scanning
- Verifieer het eindresultaat
- Veelvoorkomende problemen
- Wanneer hulp vragen
Doel
Na deze gids heb je een gelaagde container image security pipeline: Trivy-scanning in CI die builds blokkeert bij kritieke bevindingen, ondertekende images die Kyverno verifieert bij admissie, en continue in-cluster monitoring via de Trivy Operator met Prometheus-alerting.
Vereisten
- Een Kubernetes-cluster op versie 1.26 of nieuwer (voor CRD v1 en Helm OCI-ondersteuning)
kubectlgeconfigureerd met cluster-admin toeganghelmv3.8.0 of nieuwer (voor OCI chart-ondersteuning)- Een container registry waar je images naartoe pusht (Docker Hub, ECR, GCR, ACR of een private registry)
- Een GitHub Actions workflow (of vergelijkbaar CI-systeem) voor je container builds
- Kyverno geinstalleerd op je cluster als je admissie-enforcement wilt (stap 6)
- Bekendheid met Kubernetes RBAC voor het aanmaken van ServiceAccounts en ClusterRoles
Beveiligingsadvies: Trivy supply chain compromise (maart 2026)
Tussen 19 en 23 maart 2026 compromitteerde een aanvaller de distributie-infrastructuur van Aqua Security's Trivy. De aanval, gedocumenteerd in GHSA-69fq-xp46-6x23, injecteerde credential-stelende malware in Trivy-binaries (v0.69.4 tot v0.69.6), de trivy-action GitHub Action (76 van 77 versietags force-pushed naar kwaadaardige commits) en Docker Hub images (v0.69.5, v0.69.6).
Veilige versies: Trivy binary v0.69.3 of eerder, trivy-action v0.35.0, setup-trivy v0.2.6.
De oorzaak was muteerbare Git-tags. Een tag als @v0.28.0 kan op elk moment naar een andere commit verwijzen. De oplossing is simpel: pin GitHub Actions op volledige commit SHA-hashes, niet op versietags. Elke Actions-referentie in dit artikel volgt die aanpak.
Als je pipeline een gecompromitteerde versie draaide tussen 19 en 23 maart UTC, roteer dan alle secrets die toegankelijk waren voor de runner (cloudcredentials, SSH-keys, API-tokens, Docker-registrywachtwoorden). Zoek in workflow-logs naar repositories genaamd tpcp-docs-* en uitgaande verbindingen naar scan.aquasecurtiy.org (let op de spelfout).
Wat Trivy scant in een container image
Trivy analyseert elke laag van een container image, niet alleen de basis-OS-laag. Het detecteert:
- OS-pakketkwetsbaarheden (Alpine, Debian, Ubuntu, RHEL, CentOS) uit vendor security advisories
- Taaldependency-kwetsbaarheden (pip, npm, Go modules, Maven/Gradle, NuGet, Cargo, Gemfile) uit de GitHub Advisory Database, PyPI Advisory DB en anderen
- Ingebedde secrets (API-keys, tokens, credentials in image layers)
- Dockerfile-misconfiguraties (draaien als root, onnodige exposed ports) wanneer ingeschakeld met
--scanners misconfig
De kwetsbaarheidsdatabase wordt elke zes uur herbouwd vanuit NVD, GitHub Advisory Database en OS vendor advisories, en gedistribueerd via GitHub Container Registry.
Stap 1: installeer Trivy en voer je eerste scan uit
Installeer de Trivy CLI op je werkstation. Kies een methode:
# Homebrew (macOS / Linux)
brew install aquasecurity/trivy/trivy
# APT (Debian/Ubuntu)
sudo apt-get install trivy
# Of draai zonder installatie via Docker
docker run --rm aquasec/trivy:0.69.3 image nginx:1.27.4
Voer een scan uit op een publiek image om het outputformaat te zien:
trivy image nginx:1.27.4
Verwachte output (ingekort):
nginx:1.27.4 (debian 12.9)
===========================
Total: 143 (UNKNOWN: 0, LOW: 82, MEDIUM: 43, HIGH: 14, CRITICAL: 4)
┌──────────────┬────────────────┬──────────┬────────────────────┬───────────────┬──────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │
├──────────────┼────────────────┼──────────┼────────────────────┼───────────────┼──────────────────────────────────────┤
│ libssl3t64 │ CVE-2024-9143 │ CRITICAL │ 3.0.14-1~deb12u2 │ 3.0.15-1 │ OpenSSL: Low-level ... │
└──────────────┴────────────────┴──────────┴────────────────────┴───────────────┴──────────────────────────────────────┘
Elke rij koppelt een pakket aan een CVE, de ernst, de geinstalleerde versie en de versie die het fixt. Een lege "Fixed Version" betekent dat er nog geen patch beschikbaar is.
Stap 2: filter resultaten en onderdruk geaccepteerde risico's
Een onbewerkte scan rapporteert vaak honderden bevindingen, waarvan veel zonder beschikbare fix. Filter de ruis:
# Toon alleen HIGH en CRITICAL, sla ongefixte CVE's over
trivy image --severity HIGH,CRITICAL --ignore-unfixed nginx:1.27.4
Voor CVE's die je hebt beoordeeld en geaccepteerd, maak je een .trivyignore-bestand in je repository root:
# Beoordeeld op 2026-04-01 — geen exploiteerbaar pad in onze deployment
CVE-2024-9143
# Tijdelijk geaccepteerd, herbeoordelen over 90 dagen
CVE-2023-3817 exp:2026-07-01
De exp:-syntax heractivateert de bevinding automatisch na de opgegeven datum. Zo voorkom je dat onderdrukte CVE's stilletjes opstapelen.
Voor meer controle kun je het YAML-formaat gebruiken dat onderdrukking beperkt tot specifieke pakketten of paden:
# .trivyignore.yaml
vulnerabilities:
- id: CVE-2022-40897
paths:
- "usr/local/lib/python3.12/site-packages/setuptools"
expired_at: 2026-09-01
Laad het expliciet: trivy image --ignorefile .trivyignore.yaml nginx:1.27.4
Stap 3: blokkeer je CI-pipeline met GitHub Actions
Voeg Trivy toe als hard gate die je build laat falen als er CRITICAL of HIGH kwetsbaarheden met beschikbare fixes worden gevonden. Deze workflow uploadt resultaten naar GitHub's Security tab in SARIF-formaat:
# .github/workflows/image-scan.yml
name: Container image scan
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myorg/myapp:$ .
# Pin op commit SHA, niet op versietag — zie beveiligingsadvies hierboven
- name: Scan image (hard gate)
uses: aquasecurity/trivy-action@18f2510ee396bbf400402947e795cb3a0d0f5cc1 # v0.35.0
with:
image-ref: "myorg/myapp:$"
format: "table"
exit-code: "1" # laat de build falen bij bevindingen
ignore-unfixed: true # sla CVE's zonder fix over
severity: "CRITICAL,HIGH"
vuln-type: "os,library"
trivyignores: ".trivyignore"
# Tweede stap: upload SARIF voor de GitHub Security tab
- name: Scan image (SARIF report)
uses: aquasecurity/trivy-action@18f2510ee396bbf400402947e795cb3a0d0f5cc1 # v0.35.0
if: always()
with:
image-ref: "myorg/myapp:$"
format: "sarif"
output: "trivy-results.sarif"
ignore-unfixed: true
severity: "CRITICAL,HIGH"
vuln-type: "os,library"
- name: Upload SARIF naar GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: "trivy-results.sarif"
De SHA 18f2510ee396bbf400402947e795cb3a0d0f5cc1 komt overeen met v0.35.0 op het moment van schrijven. Gebruik Dependabot of Renovate om SHA-updates automatisch te volgen.
Stap 4: genereer en attesteer een SBOM
Een Software Bill of Materials legt elk pakket in je image vast. Genereer het in CycloneDX-formaat samen met je build:
trivy image --format cyclonedx --output sbom.cdx.json myorg/myapp:v1.2.0
Attesteer de SBOM aan het image met Cosign (vereist Cosign uit de volgende stap):
cosign attest --key cosign.key \
--type cyclonedx \
--predicate sbom.cdx.json \
myorg/myapp:v1.2.0
Later kan iedereen de attestatie verifieren en de SBOM scannen zonder het volledige image te pullen:
cosign verify-attestation --type cyclonedx --key cosign.pub myorg/myapp:v1.2.0
trivy sbom ./sbom.cdx.json
Dit is belangrijk omdat je zo oude images kunt herscannen tegen de nieuwste kwetsbaarheidsdatabase zonder ze opnieuw te hoeven builden.
Stap 5: onderteken images met Cosign
Cosign ondertekent container images zodat downstream consumers (waaronder Kyverno) kunnen verifieren dat een image uit je vertrouwde buildpipeline komt.
Keyless signing (aanbevolen voor CI). Gebruikt kortstondige sleutels gekoppeld aan de OIDC-identiteit van je CI-provider. Geen sleutelbeheer nodig:
# In GitHub Actions — OIDC-token is automatisch beschikbaar
cosign sign myorg/myapp:v1.2.0
Het signing-event wordt vastgelegd in Rekor, Sigstore's transparency log.
Key-based signing (voor omgevingen zonder OIDC):
# Genereer een sleutelpaar (eenmalig)
cosign generate-key-pair
# Onderteken met de private key
cosign sign --key cosign.key myorg/myapp:v1.2.0
# Verifieer met de public key
cosign verify --key cosign.pub myorg/myapp:v1.2.0
Voor productie sla je sleutels op in een KMS in plaats van op schijf:
# AWS KMS
cosign sign --key awskms://alias/image-signing myorg/myapp:v1.2.0
# GCP KMS
cosign sign --key gcpkms://projects/myproject/locations/global/keyRings/signing/cryptoKeys/cosign/cryptoKeyVersions/1 myorg/myapp:v1.2.0
# HashiCorp Vault
cosign sign --key hashivault://image-signing myorg/myapp:v1.2.0
Stap 6: verifieer image-signatures met Kyverno
Nu je images ondertekend zijn, kun je ongetekende images blokkeren op je cluster. Kyverno integreert native met Cosign hiervoor. Als je Kyverno nog niet hebt geinstalleerd, zie de vergelijking van admission controllers voor installatieopties.
Maak een ClusterPolicy die signatures verifieert voor jouw registry:
# verify-image-signature.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce # Begin eerst met Audit, schakel dan over naar Enforce
rules:
- name: check-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myorg/*" # match alle images uit jouw registry
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
Pas het toe:
kubectl apply -f verify-image-signature.yaml
Test door een ongetekend image te deployen:
kubectl run test-unsigned --image=myorg/unsigned-app:latest
Verwachte output:
Error from server: admission webhook "mutate.kyverno.svc-fail" denied the request:
resource Pod/default/test-unsigned was blocked due to the following policies:
verify-image-signature: check-signature: image verification failed
Begin met validationFailureAction: Audit om schendingen te loggen zonder ze te blokkeren. Schakel over naar Enforce als je hebt bevestigd dat alle productie-workloads getekende images gebruiken.
Stap 7: continue in-cluster scanning met de Trivy Operator
CI-scanning vangt problemen op tijdens de build, maar er verschijnen dagelijks nieuwe CVE's tegen images die al op je cluster draaien. De Trivy Operator draait als een Kubernetes Operator die automatisch workloads scant wanneer pods worden aangemaakt of bijgewerkt, en slaat resultaten op als Custom Resources.
Installeer via Helm:
helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm repo update
helm install trivy-operator aqua/trivy-operator \
--namespace trivy-system \
--create-namespace \
--set trivy.ignoreUnfixed=true
Controleer de Helm chart repository voor de nieuwste versie. De Operator heeft de status "incubating"; pin je Helm chart versie in productie.
Na installatie begint de Operator bestaande workloads te scannen. Bevraag de resultaten:
# Lijst alle vulnerability reports
kubectl get vulnerabilityreports -A
# Controleer een specifieke namespace
kubectl get vulnerabilityreports -n production -o wide
# Bekijk een specifiek rapport
kubectl describe vulnerabilityreport deployment-myapp-myapp -n production
Rapporten volgen de naamconventie <workload-kind>-<workload-name>-<container-name>. Elk rapport bevat een samenvatting met aantallen per ernstniveau en een volledige lijst van CVE's met pakketnamen, geinstalleerde versies en fixversies.
Private registries. De Operator ontdekt credentials automatisch uit imagePullSecrets in Pod specs en ServiceAccounts. Voor cloudregistries (ECR, GCR, ACR) gebruikt de Operator de workload identity van het knooppunt via de standaard cloud SDK credential chain.
Stap 8: monitor vulnerability reports met Prometheus en Grafana
De Trivy Operator exporteert Prometheus-metrics via een ServiceMonitor. Als je kube-prometheus-stack draait, ontdekt Prometheus deze metrics automatisch.
Belangrijke metrics:
| Metric | Beschrijving |
|---|---|
trivy_image_vulnerabilities |
Gauge: kwetsbaarheidsaantal per ernst, namespace, image |
trivy_resource_configaudits |
Gauge: aantal misconfiguratie-bevindingen |
trivy_image_exposedsecrets |
Gauge: aantal blootgestelde secrets |
Nuttige PromQL-queries voor Grafana-dashboards:
# Kritieke kwetsbaarheden in de productie-namespace
sum(trivy_image_vulnerabilities{namespace="production", severity="CRITICAL"})
# Top 5 meest kwetsbare namespaces
topk(5, sum by (namespace) (trivy_image_vulnerabilities))
Stel een alert in voor nieuwe kritieke bevindingen:
# trivy-alerts.yaml (PrometheusRule)
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: trivy-critical-vulns
namespace: trivy-system
spec:
groups:
- name: trivy
rules:
- alert: CriticalVulnerabilityDetected
expr: trivy_image_vulnerabilities{severity="CRITICAL"} > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Critical vulnerability in /"
Je aanvalsoppervlak verkleinen: image-minimalisatie
Scannen is reactief. Minimaliseren wat er in het image gaat is proactief en verlaagt het aantal CVE's dat je moet beheren.
Gebruik distroless of minimale base images. Chainguard Images en Google Distroless strippen shells, packagemanagers en systeemhulpprogramma's. Sysdig's scandata laat ongeveer 24 CVE's zien in een Ubuntu-gebaseerd nginx image tegenover ongeveer 2 in een distroless equivalent.
Pin image-referenties op digests. Muteerbare tags zoals :latest of :1.27 kunnen stilletjes worden bijgewerkt in een registry. Gebruik onveranderlijke SHA-digests in productiemanifesten:
# In plaats van dit
image: nginx:1.27.4
# Gebruik dit
image: nginx@sha256:4a2923e3e2e26e4c0c77b2f7b8f10b3d7a2b0f1e...
Multi-stage builds. Houd buildtools buiten het uiteindelijke image. Een Go-applicatie heeft de Go-compiler niet nodig at runtime; een Node.js-app heeft geen npm of yarn nodig:
FROM golang:1.22 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o /myapp
FROM gcr.io/distroless/static-debian12:nonroot
COPY /myapp /myapp
ENTRYPOINT ["/myapp"]
Drie misvattingen over container scanning
"Het scannen van het base image is voldoende." Container images bestaan uit meerdere lagen: de basis-OS-laag, runtimedependencies, applicatiecode en gebundelde pakketten. Een schoon Ubuntu base image zegt niks over de kwetsbare setuptools die vastzit in je Python requirements.txt. Trivy scant elke laag en elk packagemanager-manifest dat het vindt. Scan altijd het volledig gebouwde image, niet alleen de basis.
"Nul CVE's betekent dat mijn image veilig is." Nul CVE's betekent "geen bekende kwetsbaarheden in de database van deze scanner op dit moment." Er worden dagelijks nieuwe CVE's gepubliceerd. Alpine's advisory-database vermeldt alleen CVE's waarvoor een patch bestaat, dus een scanner kan nul bevindingen rapporteren simpelweg omdat er nog geen fix is uitgebracht. En CVE-scanning mist ingebedde secrets, Dockerfile-misconfiguraties, zwakke RBAC en licentieschendingen volledig.
"Je hoeft alleen te scannen tijdens de build." Een image dat zes maanden geleden is gebouwd zonder bevindingen kan vandaag tientallen bekende CVE's hebben. Admission controllers controleren images tijdens deployment maar kunnen geen nieuwe CVE's detecteren die opduiken in al draaiende containers. Daarom richt deze gids vier lagen in: build-time scanning (stap 3), SBOM-generatie voor later herscannen (stap 4), admissie-enforcement (stap 6) en continue in-cluster monitoring (stap 7).
Verifieer het eindresultaat
Na het voltooien van alle stappen, verifieer je elke laag:
# 1. CI gate: push een Dockerfile met een oud base image en bevestig dat de pipeline faalt
# 2. Image signing: verifieer je laatste image
cosign verify --key cosign.pub myorg/myapp:v1.2.0
# 3. Kyverno enforcement: probeer een ongetekend image te deployen
kubectl run test --image=docker.io/library/nginx:latest --dry-run=server
# 4. Trivy Operator: controleer of er rapporten bestaan voor draaiende workloads
kubectl get vulnerabilityreports -A | head -20
# 5. Prometheus: query voor de metric
curl -s http://prometheus:9090/api/v1/query?query=trivy_image_vulnerabilities | jq .
Als stap 3 een Kyverno-weigering oplevert en stap 4 VulnerabilityReports toont, is je gelaagde scanningpipeline operationeel.
Veelvoorkomende problemen
Trivy Operator-rapporten ontbreken voor sommige workloads. De Operator scant workloads die worden beheerd door een controller (Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob). Kale pods zonder controller worden standaard niet gescand. Controleer of de Operator-pods draaien: kubectl get pods -n trivy-system.
VulnerabilityReport overschrijdt de etcd-groottelimiet. Images met veel pakketten kunnen rapporten produceren die de 1,5 MiB per-object limiet van etcd overschrijden. Filter op ernst in de Operator-configuratie (trivy.severity: "HIGH,CRITICAL") of beperk de rapportscope.
Authenticatie bij private registry mislukt. De Operator lost credentials op uit imagePullSecrets in de Pod spec en ServiceAccount. Controleer of het secret bestaat in de namespace van de workload en of de credentials geldig zijn. Voor cloudregistries: bevestig dat de IAM-rol van het knooppunt of de workload identity pull-permissies heeft.
Kyverno blokkeert legitieme images na het inschakelen van Enforce-modus. Schakel terug naar Audit-modus, controleer PolicyReports op valse positieven en verifieer dat de public key overeenkomt met de sleutel die bij het ondertekenen is gebruikt. Gebruik cosign triangulate myorg/myapp:v1.2.0 om te bevestigen dat het signature-artifact in de registry bestaat.
Trivy database-download mislukt in air-gapped omgevingen. Download de database handmatig van Trivy's GitHub releases en host het op een interne OCI-registry met behulp van ORAS CLI. Configureer de trivy.dbRepository van de Operator om naar je interne mirror te verwijzen.
Wanneer hulp vragen
Verzamel deze details voordat je om hulp vraagt:
- Trivy CLI-versie (
trivy version) - Trivy Operator Helm chart versie en Operator pod logs (
kubectl logs -n trivy-system deploy/trivy-operator) - Kyverno-versie en admission webhook logs (
kubectl logs -n kyverno deploy/kyverno-admission-controller) - De specifieke image-referentie (met digest) die faalt
- Output van
kubectl get vulnerabilityreports -A -o wide - De inhoud van je
.trivyignore-bestand - Cosign-versie en de gebruikte signingmethode (keyless vs key-based vs KMS)