Een base image pullen, je code erin kopiëren, naar productie pushen. Die lus draaien de meesten van ons op de automatische piloot. Het stuk waar niemand naar kijkt is de base image zelf, die stilletjes een paar honderd packages meesleept die je nooit gekozen hebt. Eén ervan kan een bekende CVE dragen, en je zou er geen flauw idee van hebben.
Dat zit me dwars. Ik draai niet graag dingen die ik niet kan inspecteren, en een container image die je niet gescand hebt is precies dat: een black box die je vertrouwt omdat naar binnen kijken te veel moeite leek.
Trivy is hoe ik stopte met vertrouwen en begon met kijken. Het is een open-source vulnerability scanner van Aqua die je vertelt wat er werkelijk in een image zit en welke van die packages bekende problemen hebben. In deze post bouw ik een GitLab CI setup op van de kleinste bruikbare versie tot de versie die ik in productie draai, laag voor laag.
Waarom Trivy en niet de rest
Er zijn genoeg scanners. Snyk, Anchore en Clair doen allemaal het werk. Ik kwam uit bij Trivy om redenen die aansluiten op hoe ik dingen graag draai:
- Enkele binary. Geen database server, geen daemon, geen installatieritueel.
- Snelheid. Scans zijn klaar in seconden. Trage tools worden overgeslagen, en een overgeslagen scan beschermt niets.
- Brede dekking. OS packages, language dependencies en IaC misconfiguraties in één tool.
- CI-vriendelijk. Zinnige exit codes en een handvol output formaten.
- Gratis. Geen licenties om te tellen, geen gesprek met inkoop.
Snyk, Anchore en Clair zijn prima keuzes als ze bij jouw situatie passen. Voor mij won het gebrek aan bewegende delen van Trivy. Minder dingen om te draaien betekent minder dingen om te begrijpen als er iets stuk gaat.
De simpelste versie die werkt
Hier is de kleinste GitLab CI config die iets nuttigs doet: build, scan, en alleen pushen als de scan slaagt.
stages:
- build
- scan
- push
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $IMAGE_TAG .
- docker save $IMAGE_TAG -o image.tar
artifacts:
paths:
- image.tar
expire_in: 1 hour
scan:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --input image.tar --exit-code 1 --severity HIGH,CRITICAL
allow_failure: false
push:
stage: push
image: docker:24
services:
- docker:24-dind
script:
- docker load -i image.tar
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $IMAGE_TAG
needs:
- build
- scan
Drie dingen maken dit werkend:
- Build eerst, scan daarna. De image wordt opgeslagen als tarball artifact en doorgegeven aan de scan stage.
- Exit code 1. Trivy faalt de pipeline als hij een HIGH of CRITICAL kwetsbaarheid vindt.
- Push alleen na een geslaagde scan. Geen enkele kwetsbare image bereikt ooit de registry.
Als je hier stopt met lezen, heb je al een werkende gate. Alles hieronder is verfijning.
De output lezen
Wanneer Trivy iets vindt, krijg je een tabel als deze:
┌──────────────────┬────────────────┬──────────┬────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Installed Version │
├──────────────────┼────────────────┼──────────┼────────────────────────────────────┤
│ openssl │ CVE-2024-0727 │ HIGH │ 3.0.2-0ubuntu1.10 │
│ libcurl4 │ CVE-2024-2398 │ MEDIUM │ 7.81.0-1ubuntu1.15 │
│ python3.10 │ CVE-2024-0450 │ HIGH │ 3.10.12-1~22.04.3 │
└──────────────────┴────────────────┴──────────┴────────────────────────────────────┘
Dat is het hele punt. Je weet nu welke packages problemen hebben, hoe ernstig elk daarvan is, en welke versie getroffen is. Daarvoor was de eerlijke beschrijving van je security houding “waarschijnlijk prima”. Nu kun je de vraag echt beantwoorden.
Verder: severity drempels
Niet elke finding verdient een rode pipeline. Trivy laat je de drempel afstemmen op wat je bereid bent te shippen:
# Fail alleen op critical - snel maar risicovol
trivy image --exit-code 1 --severity CRITICAL
# Fail op high en critical - gebalanceerd
trivy image --exit-code 1 --severity HIGH,CRITICAL
# Fail op medium en hoger - strikt
trivy image --exit-code 1 --severity MEDIUM,HIGH,CRITICAL
Begin met HIGH,CRITICAL. Je kunt altijd later strenger worden. Begin op dag één te strikt en je verzuipt in MEDIUM findings, mensen gaan door het rood heen klikken zonder te lezen, en de gate wordt theater.
Verder: false positives
Soms vlagt Trivy een CVE die echt niet op jou van toepassing is. Het code pad wordt nooit geraakt, of er is simpelweg nog geen fix. Een .trivyignore bestand handelt die af:
# CVE raakt ons code pad niet
CVE-2024-0727
# Nog geen fix beschikbaar, tracking in JIRA-1234
CVE-2024-2398
Wijs de scan ernaar:
trivy image --input image.tar --ignorefile .trivyignore --exit-code 1
Schrijf op waarom je elke CVE negeert, daar in het bestand zelf. Over zes maanden weet je het niet meer, en een ongedocumenteerde ignore is gewoon een gat in je gate zonder audit trail.
Verder: output formaten
De standaard tabel is voor mensen. Trivy spreekt ook andere formaten, afhankelijk van wie of wat het leest:
# Menselijk leesbare tabel (standaard)
trivy image --format table $IMAGE_TAG
# JSON voor programmatische verwerking
trivy image --format json --output results.json $IMAGE_TAG
# SARIF voor GitHub/GitLab security dashboards
trivy image --format sarif --output trivy.sarif $IMAGE_TAG
# HTML rapport voor stakeholders
trivy image --format template --template "@contrib/html.tpl" --output report.html $IMAGE_TAG
Voor GitLab’s ingebouwde security dashboard genereer je het eigen rapportformaat:
scan:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --format template --template "@/contrib/gitlab.tpl" --output gl-container-scanning-report.json $IMAGE_TAG
- trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_TAG
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
Nu verschijnen de findings direct in de merge request, waar de persoon die de change reviewt ze ook echt ziet.
Verder: meer scannen dan alleen images
Trivy stopt niet bij container images. Dezelfde binary doet je source tree, een remote repo, en je Kubernetes of Terraform config:
# Filesystem (je project directory)
trivy fs --exit-code 1 .
# Git repository
trivy repo https://github.com/your/repo
# Kubernetes manifests
trivy config ./k8s/
# Infrastructure as Code
trivy config ./terraform/
Een vollere security stage draait er misschien een paar samen:
security-scan:
stage: scan
image: aquasec/trivy:latest
script:
# Scan source code dependencies
- trivy fs --exit-code 0 --severity HIGH,CRITICAL . --format table
# Scan Kubernetes manifests voor misconfiguraties
- trivy config ./k8s/ --exit-code 0 --severity HIGH,CRITICAL
# Scan container image (deze faalt de build)
- trivy image --input image.tar --exit-code 1 --severity HIGH,CRITICAL
Verder: de database cachen
Trivy haalt bij elke run een vulnerability database op. In CI zijn dat verspilde minuten en een externe afhankelijkheid die je niet op het kritieke pad nodig hebt. Cache het:
variables:
TRIVY_CACHE_DIR: .trivy-cache
scan:
stage: scan
image: aquasec/trivy:latest
cache:
key: trivy-db
paths:
- .trivy-cache
script:
- trivy image --cache-dir $TRIVY_CACHE_DIR --input image.tar
Of splits de download van de scan zodat je precies bepaalt wanneer de netwerkcall gebeurt:
scan:
image: aquasec/trivy:latest
before_script:
- trivy image --download-db-only
script:
- trivy image --skip-db-update --input image.tar
Het volledige plaatje: mijn productie config
Dit is wat ik daadwerkelijk draai. Het is de simpele versie van bovenaan, plus elke laag die we net doorlopen hebben:
stages:
- build
- scan
- push
- deploy
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
TRIVY_CACHE_DIR: .trivy-cache
# Fail niet op ongefixte kwetsbaarheden
TRIVY_IGNORE_UNFIXED: "true"
.trivy-template: &trivy-template
image: aquasec/trivy:latest
cache:
key: trivy-db
paths:
- .trivy-cache
scan-image:
<<: *trivy-template
stage: scan
script:
# Genereer rapport voor GitLab dashboard
- trivy image
--cache-dir $TRIVY_CACHE_DIR
--format template
--template "@/contrib/gitlab.tpl"
--output gl-container-scanning-report.json
--input image.tar
# Fail op fixbare HIGH/CRITICAL
- trivy image
--cache-dir $TRIVY_CACHE_DIR
--exit-code 1
--severity HIGH,CRITICAL
--ignore-unfixed
--input image.tar
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
needs:
- build
scan-config:
<<: *trivy-template
stage: scan
script:
- trivy config
--cache-dir $TRIVY_CACHE_DIR
--exit-code 1
--severity HIGH,CRITICAL
./k8s/
allow_failure: true # Alleen waarschuwing, niet blokkeren
Drie beslissingen daarin zijn het benoemen waard, want het zijn trade-offs en geen vanzelfsprekende defaults:
--ignore-unfixed. Falen op een kwetsbaarheid zonder beschikbare patch blokkeert me terwijl er niets te doen valt. Ik weet er liever van zonder dat het de lijn stillegt.- Config scan als waarschuwing. Misconfiguraties zijn belangrijk, maar ik wil niet dat een losse
k8s/muggenzifterij elke deploy blokkeert. Het zeurt, het gate niet. - Cached database. Snellere builds en één externe afhankelijkheid minder in het hete pad.
De pipeline is het vangnet, niet de eerste lijn
Er zijn twee manieren om na te denken over waar scanning thuishoort. Gate keeping zet het op build time: scan de image, blokkeer de slechte van de registry, handhaaf de standaard voordat er iets deployt. Shift left duwt het eerder, in de handen van de developer met pre-commit hooks en IDE plugins, zodat feedback in seconden komt in plaats van na een pipeline run.
Ik draai beide. De pipeline vangt wat erdoorheen glipt, maar een developer hoort het probleem te zien voordat hij überhaupt commit:
# Developer workflow
trivy fs . # Scan voor commit
docker build -t myapp .
trivy image myapp # Scan voor push
Zie de pipeline als de veiligheidsgordel. Nuttig precies wanneer er al iets is misgegaan, maar je leunt er liever niet als primair plan op.
Herscannen bij promotie
Als je met ArgoCD deployt, hang er dan nog één scan aan de promotiestap:
promote-to-prod:
stage: deploy
script:
# Herscan de specifieke image versie voor promotie
- trivy image --exit-code 1 --severity CRITICAL $IMAGE_TAG
# Update GitOps repo alleen als scan slaagt
- |
cd gitops-repo
kustomize edit set image myapp:${CI_COMMIT_TAG}
git commit -am "Promote ${CI_COMMIT_TAG} to production"
git push
rules:
- if: $CI_COMMIT_TAG =~ /^v[0-9]+/
Een image die op build time schoon was, is niet gegarandeerd een week later nog schoon, want er komen voortdurend nieuwe CVE’s bij. Opnieuw scannen vlak voordat je de nieuwe tag in de GitOps repo schrijft, betekent dat een kwetsbaarheid die na de originele build bekend werd alsnog gevangen wordt voordat hij productie raakt.
Verder dan kwetsbaarheden: de SBOM
Een Software Bill of Materials is een inventaris van alles in je image. Trivy genereert er een in één commando:
trivy image --format spdx-json --output sbom.json $IMAGE_TAG
Het is om een paar redenen het hebben waard:
- Compliance. Sommige industrieën vereisen inmiddels standaard een SBOM.
- Incident response. Wanneer de volgende grote CVE uitkomt, grep je je opgeslagen SBOMs in plaats van onder druk elke image te rebuilden en herscannen.
- Supply chain zichtbaarheid. Je kent je dependencies voordat iemand anders je erop wijst.
Sla de SBOM op naast de image in je registry, zodat hij er is wanneer je hem nodig hebt.
Veelvoorkomende valkuilen
Het verkeerde scannen
# Fout - scant de trivy image zelf
trivy image aquasec/trivy:latest
# Goed - scant je gebouwde image
trivy image --input image.tar
De exit code inslikken
# Fout - slaagt altijd
trivy image $IMAGE_TAG || true
# Goed - fail bij findings
trivy image --exit-code 1 $IMAGE_TAG
Alert fatigue
Begin op MEDIUM zonder ignores en je krijgt op dag één honderden findings. Mensen stoppen met kijken, en een gate die niemand leest is gewoon latency. Begin strikt op critical, breid uit naarmate je schoonmaakt.
Waarom ik de moeite neem
Dit ging nooit over het afvinken van een compliance vakje. Het gaat erom dat ik een simpele vraag kan beantwoorden wanneer de volgende kritieke CVE wordt aangekondigd: welke van mijn images zijn getroffen, welke services draaien ze, en hoe snel kan ik patchen?
Zonder scanning is elk van die vragen een handmatige zoektocht door elke image die je ooit hebt geshipt, gedaan onder tijdsdruk met iemand die meekijkt. Met Trivy in de pipeline staat het antwoord al voor je. Dat is het verschil tussen weten wat je draait en hopen dat het prima is.
Je kunt niet beschermen wat je niet kunt zien. Container scanning maakt de inhoud van je images zichtbaar, en verandert black boxes in gedocumenteerde, auditeerbare systemen.
