Nous allons, dans cet article, traiter deux incidents DevSecOps liés à Trivy: La première est une défaillance surevenue à l'installation de l'action, suite à une mise à jour dependabot. La seconde est la détection de vulnérabilités critiques dans une image Docker distroless utilisée en production.
Les deux situations sont bloquantes pour le build.
Les deux issues ont été rencontrées le même jour donc, ici l'une résolution servira à la suivante.
Plusieurs solutions peuvent être envisagées selon le contexte et, plusieurs ajustements sont parfois nécessaires pour conserver un pipeline stable tout en maintenant le niveau de sécurité élevé.
bash ./trivy/contrib/install.sh -b ... v0.65.0
...
found version: 0.65.0
Error: Process completed with exit code 1
Il s'agit d'une limitation du mécanisme d'installation de Trivy utilisé par l'action. L'action clone le dépôt, exécute contrib/install.sh et télécharge le binaire.
Certaines versions de Trivy, comme v0.65.0, provoquent l'échec de ce script d'installation. Le téléchargement se termine correctement, la version est bien détectée, mais le script install.sh échoue ensuite.
Il est recommandé de vérifier les sources officielles sur aquasec.com pour obtenir les dernières informations sur la sécurité de leurs dépôts
A la date du 03/04, les versions 0.69.2 et 0.69.3 étaient confirmées sûres. - avd.aquasec.com/misconfig/dockerfile advisories GHSA-69fq-xp46-6x23À la suite de plusieurs itérations sur le pipeline CI/CD, la configuration de Trivy a été revue de manière significative.
L'objectif était d'obtenir une installation plus fiable, plus prévisible et plus facile à maintenir.
- name: Cache Trivy DB
uses: actions/cache@v5.0
with:
path: ~/.cache/trivy
key: trivy-db-${{ runner.os }}-${{ github.run_number }}
restore-keys: |
trivy-db-${{ runner.os }}-
Cette approche permet de mieux contrôler le cache, la version installée et les options utilisées. Trivy n'a plus qu'à vérifier les nouvelles failles publiées depuis le dernier scan
La configuration suit la méthode officielle recommandée pour installer la version stable de Trivy, via le dépôt APT d'Aqua Security sur une distribution Debian/Ubuntu.
- name: Install Trivy (official repo)
run: |
sudo apt-get update
sudo apt-get install -y wget gnupg lsb-release
wget -qO - https://get.trivy.dev/deb/public.key | \
gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://get.trivy.dev/deb generic main" | \
sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install -y trivy
UPDATE au 05/05/26; la version 0.69 installée après l'incident, à été retirée du dépot, à ce jour la version 0.70 est disponible:
Preparing to unpack .../trivy_0.70.0_amd64.deb ...
Unpacking trivy (0.70.0) ...
Setting up trivy (0.70.0) ...
TRIVY_VERSION="0.70.0"
if apt-cache madison trivy | grep -q "$TRIVY_VERSION"; then
sudo apt-get install -y trivy=$TRIVY_VERSION
else
echo "[WARNING] Version not found → fallback to latest"
sudo apt-get install -y trivy
fi
trivy --version
- name: Run Trivy scan
run: |
trivy fs . \
--severity HIGH,CRITICAL \
--format table \
--exit-code 0
- name: Run Trivy scan
run: |
trivy fs . \
--format table \
--exit-code 1 \
--severity HIGH,CRITICAL \
--no-progress
--exit-code 0 affiche toujours l'intégralité du tableau des vulnérabilités dans les logs mais ne bloque pas le build.
Cela nécessite une consultation manuelle: en cliquant sur l'étape du scan depuis l'interface de la CI et lire les logs.
--exit-code 1 bloque le build à la moindre vulnérabilités correspondant aux critères (HIGH/CRITICAL)
--no-progress désactive la barre de progression interactive dans la console. C'est une option essentielle en CI/CD
L'installation de trivy en manuel résouds tout les conflits de sha que j'ai eu ces derniers jours, sha updaté par dependabot > fix avec version stable > remplacement de cette version par l'installation manuelle. Seule cette installation s'est déroulée sans problème.
Dans un premier temps j'avais testé sans fixer de version pour savoir laquelle, d'apres eux, allait être téléchargée. Ensuite j'ai regardé les logs et fixé la version
→ Le process est conservé et à été adapté pour la nouvelle image (cas ou l'histoire se repéterait)
Le Dockerfile utilisé reste volontairement minimal afin de réduire la surface d'attaque, ce qui correspond à l'approche des images distroless.
Ce type d'image ne contient pas les outils classiques de gestion de paquets, ce qui empêche un simple apt-get upgrade dans le Dockerfile
Cette image embarque des paquets Debian, dont libpng, qui peuvent être signalés comme vulnérables tant que la version corrigée n'est pas disponible dans l'image publiée.
┌───────────────────────────────────┬────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├───────────────────────────────────┼────────┼─────────────────┤
│ flashcards:49aaf1d (debian 12.13) │ debian │ 2 │
├───────────────────────────────────┼────────┼─────────────────┤
│ app/app.jar │ jar │ 0 │
└───────────────────────────────────┴────────┴─────────────────┘
┌─────────────┬────────────────┬──────────┬────────┬───────────────────┬──────────────────┬─────────────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├─────────────┼────────────────┼──────────┼────────┼───────────────────┼──────────────────┼─────────────────────────────────────────────────────────┤
│ libpng16-16 │ CVE-2026-33416 │ HIGH │ fixed │ 1.6.39-2+deb12u3 │ 1.6.39-2+deb12u4 │ libpng: arbitrary code execution due to use-after-free │
│ │ │ │ │ │ │ vulnerability │
│ ├────────────────┤ │ │ │ ├─────────────────────────────────────────────────────────┤
│ │ CVE-2026-33636 │ │ │ │ │ libpng: information disclosure and denial of service │
│ │ │ │ │ │ │ via out-of-bounds read/write │
└─────────────┴────────────────┴──────────┴────────┴───────────────────┴──────────────────┴─────────────────────────────────────────────────────────┘
Le statut fixed indique qu'un correctif existe, mais qu'il n'est pas encore intégré dans l'image analysée.
Important : Les vulnérabilités détectées par Trivy dans ce cas ne proviennent pas du code applicatif ni du JAR, mais de l'image de base (distroless → Debian 12). Elles concernent des bibliothèques système (glibc, zlib, etc.) embarquées dans l'image.
L'approche retenue consiste à vérifier si une version plus récente de gcr.io/distroless/java17-debian12 intègre le correctif Debian deb12u4.
Si le correctif n'est pas encore disponible, la base actuelle est conservée avec une exception temporaire.
docker pull gcr.io/distroless/java17-debian12:nonroot
docker pull gcr.io/distroless/java17-debian12:debug-nonroot
docker manifest inspect "$IMAGE" | jq -r '.config.digest'
Avec crane: sans pull - plus rapide - déterministe
crane digest $IMAGE
IMAGE="gcr.io/distroless/java17-debian12:latest"
docker pull $IMAGE
2. Inspecter le digest local:
docker image inspect "$IMAGE" /
--format '{{index .RepoDigests 0}}'
Fonctionne uniquement si l'image est déjà locale OU que l'on fait un pull avant
3. Lancer un scan local (trivy):trivy image $IMAGE
Pour confirmer la présence de deb12u4, il faut soit scanner l'image, soit vérifier le paquet embarqué via Trivy ou un SBOM.
$ docker pull gcr.io/distroless/java17-debian12:latest
latest: Pulling from distroless/java17-debian12
Digest: sha256:0210d5b77dfbe851916184405ddfc8297e7bc652029f7a485274cb71ea2462bc
Status: Image is up to date for gcr.io/distroless/java17-debian12:latest
gcr.io/distroless/java17-debian12:latest
$ docker image inspect gcr.io/distroless/java17-debian12:latest --format '{{.Id}} {{.RepoDigests}}'
sha256:0210d5b77dfbe851916184405ddfc8297e7bc652029f7a485274cb71ea2462bc [gcr.io/distroless/java17-debian12@sha256:0210d5b77dfbe851916184405ddfc8297e7bc652029f7a485274cb71ea2462bc]
Pour la comparaison de l'étape 3: Comme je n'ai ni "Trivy" ni "crane" en local;
un script bash a été mis en place pour comparer automatiquement le SHA utilisé dans le pipeline avec celui de l'image latest
| Méthode | Fiabilité |
|---|---|
| crane | parfaite |
| docker pull + inspect | fiable |
| manifest inspect | fiable mais moins pratique pour l'automatisation |
J'utiliserai donc: "Docker" en local et "crane" dans le script et en CI (installation)
Retrouver le script dans le repository
#!/usr/bin/env bash
set -euo pipefail
IMAGE="gcr.io/distroless/java17-debian12:latest"
STATE_FILE="ci-scripts/.distroless-java17-debian12.digest"
echo "[INFO] Checking crane availability..."
if ! command -v crane >/dev/null 2>&1; then
echo "[WARNING] crane not found → skipping digest check"
echo "digest_changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "[INFO] Fetching current digest..."
CURRENT_DIGEST=$(crane digest "$IMAGE")
if [[ -f "$STATE_FILE" ]]; then
PREVIOUS_DIGEST=$(cat "$STATE_FILE")
else
PREVIOUS_DIGEST=""
fi
echo "[INFO] Current: $CURRENT_DIGEST"
echo "[INFO] Previous: $PREVIOUS_DIGEST"
if [[ "$CURRENT_DIGEST" == "$PREVIOUS_DIGEST" ]]; then
echo "[INFO] No update detected"
echo "digest_changed=false" >> "$GITHUB_OUTPUT"
else
echo "[INFO] Update detected"
echo "digest_changed=true" >> "$GITHUB_OUTPUT"
echo "digest_value=$CURRENT_DIGEST" >> "$GITHUB_OUTPUT"
fi
Script conçu pour optimiser les pipelines en détectant si une image de base (ici une image distroless de Google) a réellement changé
Étape par étape :Récupération du Digest: On utilise crane pour extraire l'identifiant unique et immuable de l'image, plutôt que de se fier au tag :latest qui peut pointer vers différentes versions
Comparaison d'État: Il compare ce nouveau digest avec celui stocké et versionné (.distroless-java17-debian12.digest)
sha256:0210d5b77dfbe851916184405ddfc8297e7bc652029f7a485274cb71ea2462bc
Confirme que l'image correspond à la dernière version publiée pour ce tag, mais pas nécessairement que les correctifs de sécurité les plus récents y sont intégrés
UPDATE * au 05/05/2026, J'ai migré vers la version "debian13:nonroot" ce qui à résolu les vulnérabilité en cours. Les scripts et pipelines ont été adaptés également et tourne toujours, mais pour la nouvelle image
IMAGE="${1:-gcr.io/distroless/java17-debian13:nonroot}"
STATE_FILE="${2:-ci-scripts/.distroless-java17-debian13.digest}"
Agit pleinement pour main où le controle est + strict (exit 1)
En staging on voit toutes les vulnérabilité (exit 0) sans bloquer le build (util pour vérifier, être à jour)
vulnerabilities:
- id: CVE-2026-33416
expired_at: 2026-07-15
statement: Temporary ignore until distroless is rebuilt with deb12u4
- id: CVE-2026-33636
expired_at: 2026-07-15
statement: Temporary ignore until distroless is rebuilt with deb12u4
La base actuelle est conservée, avec l'ajout de ce fichier. Il contient les exceptions signalées au Scan.
Une date d'expiration est ajoutée afin d'éviter qu'elles soient ignorées de façon permanente.
Cette pratique doit rester exceptionnelle et temporaire. Le correctif doit être apporté via une mise à jour de l'image de base ou un changement d'image mère
L'objectif est de vérifier chaque jour si une nouvelle version de l'image de base Distroless est disponible. Si c'est le cas, le workflow créera automatiquement une branche et une Pull Request est envoyée pour mettre à jour le fichier .digest - Ce workflow garantit que l'application utilise toujours la version la plus sûre de l'image
name: Distroless Watch
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * *'
permissions:
contents: write
pull-requests: write
jobs:
watch:
runs-on: ubuntu-24.04
steps:
- name: Checkout
- name: Setup git
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
- name: Install crane
run: |
ci-scripts/install-crane.sh
- name: Run digest check
id: check
run: |
chmod +x ci-scripts/check-docker-image-latest.sh
ci-scripts/check-docker-image-latest.sh
- name: Create branch with updated digest
if: steps.check.outputs.digest_changed == 'true'
id: branch
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
run: |
EXISTING=$(gh pr list \
--base main \
--search "distroless" \
--json number \
--jq length)
if [ "$EXISTING" -gt 0 ]; then
echo "Existing PR detected, skipping"
echo "skip_pr=true" >> $GITHUB_OUTPUT
exit 0
fi
BRANCH="chore/distroless-update-$(date +%s)"
git checkout -b $BRANCH
echo "${{ steps.check.outputs.digest_value }}" >
ci-scripts/.distroless-java17-debian12.digest
git add ci-scripts/.distroless-java17-debian12.digest
git commit -m "chore: update distroless digest"
git push origin $BRANCH
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
- name: Create PR
if: steps.check.outputs.digest_changed == 'true'
&& steps.branch.outputs.skip_pr != 'true'
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
run: |
BRANCH="${{ steps.branch.outputs.branch }}"
gh pr create \
--base main \
--head "$BRANCH" \
--title "chore: distroless image updated" \
--body "Automatic update of distroless base image.\
n\n Digest updated -> possible security fixes available.\
n\n Actions recommended:\n- Run pipeline\
n - Verify Trivy results\n- Check if CVEs
can be removed from .trivyignore"
workflow_dispatch: Permet de lancer le scan manuellement depuis l'interface GitHub
schedule: S'exécute automatiquement tous les jours à 06h00 UTC via une tâche cron
Runner: S'exécute sur une machine virtuelle Ubuntu 24.04
- name: Setup git
Configure un utilisateur Git temporaire, afin d'effectuer les commits automatisés
- name: install crane
d'abords en curl, ensuite depuis le script bash
- name: Run digest check
Exécute le script qui détectera la mise à jour
Le script compare le "digest" de l'image actuelle avec la dernière version en ligne. Le résultat est stocké dans une variable
- name: Create branch with updated digest
Si une mise à jour est détectée:- name: Create PR
Utilise la CLI GitHub et le jeton secret GH_PAT pour créer la PR
Run chmod +x ci-scripts/check-docker-image-latest.sh
No update: gcr.io/distroless/java17-debian12:latest is unchanged.
Digest: sha256:0210d5b77dfbe851916184405ddfc8297e7bc652029f7a485274cb71ea2462bc
Create branch with updated digest
Create PR
Le sha n'ayant pas changé, il ne créera donc pas de nouvelle branche, ni de nouvelle PR
On commence par tester l'environement [staging] pour voir les logs, être averti de la moindre vulnérabilité, avant de faire tourner le pipeline en production
Retrouver tous les workflows dans le repository
- name: Install crane
run: |
curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz \
| tar -xz crane
sudo mv crane /usr/local/bin/
→ Une fois la version connue, depuis "releases/latest"
→ On "pin" la version dans le script bash avec:
CRANE_VERSION="v0.21.5"
→ Et ensuite, changer l'url vers
go-containerregistry/releases/download/${CRANE_VERSION}/
- name: Install crane
id: install
run: |
ci-scripts/install-crane.sh
git update-index --chmod=+x ci-scripts/install-crane.sh
Une fois, avant le commit- name: Check distroless digest
id: digest
run: |
chmod +x ci-scripts/check-docker-image-latest.sh
ci-scripts/check-docker-image-latest.sh
- name: Debug FULL outputs
run: |
echo "digest_changed = '${{ steps.digest.outputs.digest_changed }}'"
echo "digest_value = '${{ steps.digest.outputs.digest_value }}'"
- name: Build Docker image
- name: Install Trivy (official repo)
//... sudo apt-get install -y trivy
- name: Scan Docker image (Trivy - conditional)
run: |
if [ "${{ steps.digest.outputs.digest_changed }}" = "true" ]; then
echo "[INFO] New distroless → strict scan"
trivy image flashcards:staging \
--severity HIGH,CRITICAL \
--ignorefile /dev/null \
--exit-code 0 \
--format table
else
echo "[INFO] Stable distroless → known CVEs list"
trivy image flashcards:staging \
--severity HIGH,CRITICAL \
--ignorefile .trivyignore.yaml \
--exit-code 0 \
--format table
fi
Branchement Logique (if/else du scan):
if: Nouveau Digest Action: Scan strict
else: Digest Stable Action: Scan standard
steps:
- name: Cache Trivy DB
- name: Install Trivy (official repo)
- name: Run Trivy scan
//..après "Build Docker image"
- name: Install crane
- name: Check distroless digest
- name: Debug FULL outputs
- name: Trivy Scan Docker Image (new distroless)
if: steps.digest.outputs.digest_changed == 'true'
run: |
trivy image $IMAGE_NAME:$IMAGE_TAG \
--severity HIGH,CRITICAL \
--exit-code 1 \
--ignore-unfixed \
--ignorefile /dev/null \
--timeout 10m \
--cache-dir ~/.cache/trivy \
--format table \
--no-progress
- name: Trivy Scan Docker Image (stable distroless)
if: steps.digest.outputs.digest_changed == 'false'
run: |
trivy image $IMAGE_NAME:$IMAGE_TAG \
--severity HIGH,CRITICAL \
--exit-code 1 \
--ignore-unfixed \
--ignorefile .trivyignore.yaml \
--timeout 10m \
--cache-dir ~/.cache/trivy \
--format table \
--no-progress
- name: Update digest file
if: success() && steps.digest.outputs.digest_changed == 'true'
run: echo "${{ steps.digest.outputs.digest_value }}" >
ci-scripts/.distroless-java17-debian12.digest
Cas A: Nouveau Digest - Action: Scan strict
Cas B: Digest Stable - Action: Scan standard
Mets à jour le digest après la vérification et le status "success" de l'étape précédente " Trivy Scan Docker Image (new distroless)"
Cet incident met en évidence une réalité importante en DevSecOps:
La sécurité ne dépend pas uniquement du code applicatif, mais de l'ensemble de la "supply chain" (images, dépendances, outils CI/CD)
La mise en place combinée de Trivy, d'un suivi automatisé du digest et d'une gestion contrôlée des exceptions permet de maintenir un pipeline stable et sécurisé
Bien que certaines des opérations soient encore manuelles (digest à modifier) à ce stade, elles jouent un rôle d'observation pour l'instant. Lorsqu'une mise à jour se présentera, j'adapterai le système pour qu'il soit en concordance avec la stratégie Gitlab Flow (PR vers develop).
Run chmod +x ci-scripts/check-docker-image-latest.sh
[INFO] Checking crane availability...
[INFO] Fetching current digest...
[INFO] Current: sha256:81d09cac6ec47f6a13c61a941557f95079213320f3ddbf9d353de9317669aab5
[INFO] Previous: sha256:b0e67f7fa5649297e655e37b2cd67471d7d38c2f8617790e9d8e8eab78ed6bcb
[INFO] Update detected
Entretemps, à l'étape précédente, j'avais ajouté les étapes du "scan de l'image Docker (Trivy)" en [staging], pour pouvoir tester l'image avant qu'elle n'arrive en production. Ce qui est déja bien, à résolu les vulnérabilités et le système de vérification du digest est lancé tous les jours à 06:00 UTC.
Cependant, l'action de changer le digest restant manuelle, j'avais choisi d'en rester là jusqu'à ce que le digest puisse être mis à jour, ce qui est chose faite à présent, je vais pouvoir automatiser.