Le problème
Prenons une boutique en ligne organisée en monorepo. Les
services Symfony order, stock,
shipping et billing côtoient des
fronts Vue.js, et moon pilote les tâches. À chaque commit,
la CI construit une image par service, la pousse dans un
registre, et chaque environnement la déploie par digest.
Ce pipeline exécute beaucoup de code que l'équipe n'a pas écrit : actions tierces, images de base, scanner de vulnérabilités, paquets Composer et npm. Ce code tourne avec les droits du pipeline : jeton du registre, identité de signature, parfois accès à la recette. C'est la chaîne d'approvisionnement du logiciel.
Les symptômes se ressemblent d'un dépôt à l'autre :
-
uses: vendor/scan-action@v1: le tagv1peut désigner demain un autre commit ; -
FROM php:8.4-fpm-alpine: deux builds du même commit ne partent pas forcément de la même image ; -
curl -sSfL https://... | shinstalle la dernière version d'un outil, sans vérification ; -
personne ne sait dire quelles versions de bibliothèques
tournent dans l'image
orderen production.
Ce risque s'est concrétisé. Le 19 mars
2026, un attaquant muni d'identifiants compromis a publié
une version piégée de Trivy, le scanner d'Aqua Security
(CVE-2026-33634). Il a aussi redirigé 76 des 77 tags de
aquasecurity/trivy-action, et tous ceux de
aquasecurity/setup-trivy, vers un code qui
volait les secrets.
Selon l'avis de sécurité, les workflows qui référençaient ces actions par tag pendant la fenêtre d'exposition ont exécuté ce code. Ceux qui les épinglaient sur le SHA d'un commit sain n'ont pas été touchés, pas plus que les images de Trivy référencées par digest. L'outil chargé de la sécurité était devenu la porte d'entrée.
Les options
Suivre les tags et les dernières versions
- Ce qu'elle règle : rien à maintenir, les correctifs arrivent seuls.
- Ce qu'elle coûte : le code exécuté change sans relecture. Un tag déplacé ou une version piégée entre dans le pipeline au build suivant.
Épingler des numéros de version
- Ce qu'elle règle : chaque mise à jour devient une décision visible dans le dépôt.
- Ce qu'elle coûte : un tag Git ou un tag d'image reste modifiable par qui contrôle le dépôt de l'éditeur. La version affichée ne garantit pas le contenu.
Épingler des empreintes et automatiser leur mise à jour
- Ce qu'elle règle : un SHA de commit, un digest d'image ou une somme de contrôle désignent un contenu précis. Un tag réécrit chez l'éditeur ne change plus rien chez vous.
- Ce qu'elle coûte : des références illisibles sans commentaire, et un flux régulier de pull requests de mise à jour à relire.
Tout reconstruire en vase clos
- Ce qu'elle règle : outils compilés depuis les sources, dépendances servies par des miroirs internes, aucun téléchargement public pendant le build.
- Ce qu'elle coûte : une exploitation lourde. Il faut tenir les miroirs, recompiler les outils et suivre soi-même les avis de sécurité.
Notre recommandation
Épingler par empreinte tout ce que la CI exécute, faire passer les dépendances par un proxy, et livrer chaque image signée avec son SBOM.
L'épingle fixe ce qui s'exécute. Un robot propose les mises à jour, la revue de code les accepte : chaque changement de version devient un diff relu.
| Ce que la CI exécute | Épinglé par | Ce que l'épingle empêche |
|---|---|---|
Actions (uses:) |
SHA de commit complet | un tag déplacé vers un autre code |
| Images de base et de test | digest sha256: |
une image republiée sous le même tag |
| Outils en ligne de commande | version et somme de contrôle | un binaire remplacé chez l'éditeur |
| Paquets Composer et npm | lockfile, installé via un proxy | une version imprévue, un téléchargement non tracé |
Le SBOM (Software Bill of Materials) est l'inventaire des composants d'une image, dans un format standard. Il répond à la question « cette bibliothèque vulnérable tourne-t-elle en production ? » sans reconstruire l'image. La signature, elle, permet au déploiement de refuser une image que la CI n'a pas produite.
CycloneDX ou SPDX
Deux formats de SBOM coexistent. CycloneDX, projet de l'OWASP normalisé par Ecma International (ECMA-424), en est à la version 1.7. SPDX, projet de la Linux Foundation, est normalisé à l'ISO dans sa version 2.2.1 (ISO/IEC 5962:2021) ; sa version courante est la 3.0.1.
Syft et Trivy produisent les deux formats. Le choix compte moins que la constance : un seul format, produit par le même outil à chaque build, pour comparer deux versions d'une image.
Les compromis assumés
Un flux continu de mises à jour. Chaque épingle devient une pull request quand l'éditeur publie. Regroupez-les par écosystème et tenez une cadence, sinon la pile grossit et plus personne ne les lit. Une épingle jamais mise à jour finit par figer une version vulnérable.
Le SHA ne couvre que la référence directe.
Une action composite peut appeler d'autres actions par tag.
Dans l'incident Trivy, une
trivy-action épinglée sur un commit antérieur
au 9 avril 2025 récupérait encore
setup-trivy par tag, et restait exposée.
La documentation de GitHub ne précise pas si la règle
d'épinglage couvre les actions appelées par une action
composite : vérifiez-le. La règle ne voit pas non plus un
outil que l'action télécharge elle-même : nous lisons donc
le action.yml des actions retenues. GitHub
prépare un fichier de verrouillage pour ces dépendances
transitives, en préversion technique au moment où nous
écrivons.
Le proxy devient un point critique. S'il tombe, aucun build ne passe : surveillez-le et sauvegardez-le comme la CI. Il trace les téléchargements et permet de bloquer une version, mais il ne remplace ni l'audit ni la relecture des mises à jour.
La signature sans clé publie l'identité du
workflow.
En mode keyless, Sigstore inscrit chaque signature dans son
journal de transparence public, Rekor, avec un certificat
qui nomme le dépôt et le workflow. Pour un dépôt privé ou
une forge auto-hébergée en zone isolée, signez plutôt avec
une clé gérée dans un KMS (cosign sign --key).
Avec cosign 3, --tlog-upload=false est refusé :
passez à --signing-config une configuration
sans journal, créée par
cosign signing-config create.
Le signal qui doit faire réexaminer ces choix : des audits contournés, des mises à jour qui s'empilent, une vérification de signature désactivée « temporairement ». Ce sont les signes d'un dispositif trop bruyant pour être tenu.
Mise en œuvre
Les exemples utilisent GitHub Actions. Le principe vaut ailleurs : GitLab CI accepte un SHA de commit pour ses composants et un digest pour ses images. Azure Pipelines référence ses tâches par version : vérifier les outils par somme de contrôle reste à votre charge.
1. Faire l'inventaire
Listez ce que la CI télécharge et exécute :
grep -rn "uses:" .github
grep -rn "^FROM" --include=Dockerfile services fronts
grep -rn -e "curl" -e "wget" .github/workflows tools
Chaque référence à un tag, à latest ou à une
URL sans somme de contrôle est à épingler.
2. Épingler les actions par SHA
Chaque uses: désigne le SHA complet d'un
commit, suivi de la version en commentaire :
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
# v7.0.1. Vérifiez que ce SHA vient du dépôt de l'action, et non
d'un fork. Le workflow complet figure à l'étape 6.
Activez ensuite la règle « Require actions to be pinned to a
full-length commit SHA », au niveau de l'organisation ou du
dépôt. Un workflow qui référence une action par tag échoue
alors au lancement. Déclarez aussi
permissions: contents: read en tête de workflow
: chaque job n'élargit le GITHUB_TOKEN que pour
ses besoins.
3. Épingler les images par digest
# services/order/Dockerfile (extrait)
FROM composer:2.10@sha256:af98f42dfff7c68ba8d53c2164fd9fde1087b7d449514baa38c418b1f6bc4bac AS composer
FROM php:8.4-fpm-alpine@sha256:78cd8de9970a9cd6ff4d98860a94eb5bf37dd2d5776b4785562dbfad01c29d5a
COPY --from=composer /usr/bin/composer /usr/bin/composer
Les images des services de test, comme PostgreSQL dans un job, s'épinglent de la même façon. Dependabot ne les met pas à jour dans un workflow, Renovate si.
Dependabot tient à jour les SHA, les digests des Dockerfile et les lockfiles. Il met à jour le commentaire de version avec le SHA, et regroupe les mises à jour par écosystème :
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns: ['*']
- package-ecosystem: docker
directories: ['/services/*', '/fronts/*']
schedule:
interval: weekly
Les blocs composer et npm suivent
le même modèle.
Dependabot attend par défaut trois jours avant de proposer
une nouvelle version (option cooldown), sauf
pour les mises à jour de sécurité. Ce délai réduit le risque
d'adopter une version piégée, retirée peu après sa
publication. Avec Renovate, le preset
helpers:pinGitHubActionDigests épingle les
actions de la même façon.
4. Installer les outils avec leur somme de contrôle
#!/usr/bin/env sh
# tools/install-syft.sh : version et somme de contrôle versionnées dans le dépôt
set -eu
SYFT_VERSION=1.52.0
SYFT_SHA256=caeedb81fb0491615f1ebd1761e4145d41ee86dd2cc7bf80669f9f5ad9d6133d
curl -sSfLo syft.tar.gz \
"https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_linux_amd64.tar.gz"
echo "${SYFT_SHA256} syft.tar.gz" | sha256sum --check
tar -xzf syft.tar.gz syft
La somme vient du fichier
syft_1.52.0_checksums.txt de la release. Ce
fichier est signé avec Sigstore : vérifiez sa signature
(cosign verify-blob) quand vous relevez la
somme, pas à chaque build. Pour les outils installés par
proto, .prototools fixe déjà la version ; son
fichier de verrouillage, qui ajoute les sommes de contrôle,
reste marqué instable.
5. Proxy, lockfiles et audit des dépendances
Un proxy de dépendances est le seul point de passage entre la CI et les registres publics. Nexus Repository Pro ou Cloud et Artifactory gèrent Composer et npm, Verdaccio se limite à npm, Private Packagist sert de miroir Composer. Chaque téléchargement y laisse une trace, et une version suspecte se bloque à un seul endroit.
# services/order : Composer passe par le proxy, plus par packagist.org
composer config repositories.proxy composer https://proxy.example.com/composer/
composer config repo.packagist.org false
# fronts/shop : npm passe par le proxy
npm config set registry https://proxy.example.com/npm/ --location=project
La CI installe depuis les lockfiles, avec
composer install et npm ci, jamais
composer update ni npm install. Le
package-lock.json enregistre l'empreinte
(integrity) de chaque archive.
Puis chaque projet déclare une tâche moon
audit, que moon ci lance avec les
autres vérifications :
# services/order/moon.yml (extrait)
tasks:
audit:
command: 'composer audit --locked'
inputs:
- 'composer.lock'
options:
# La base d'avis change sans que le lockfile change :
# pas de cache, et un lancement à chaque build.
cache: false
runInCI: 'always'
Avec Composer 2.10, composer audit sort avec le
code 1 dès qu'un paquet enfreint une politique : avis de
sécurité, paquet abandonné ou signalé comme malveillant.
Côté front, npm audit échoue dès la première
vulnérabilité, --audit-level fixe un seuil, et
npm audit signatures vérifie les signatures du
registre. Les deux outils interrogent les dépôts configurés
: vérifiez que votre proxy relaie les avis et les clés de
signature.
6. Produire le SBOM, signer et attester l'image
Le workflow construit l'image de chaque service, puis produit son SBOM, la signe et attache le SBOM sous forme d'attestation signée. La matrice est fixe pour rester lisible ; en pratique, elle reprend les projets affectés calculés par moon dans le workflow de CI.
# .github/workflows/images.yml
name: images
on:
push:
branches: [main]
permissions:
contents: read
jobs:
image:
runs-on: ubuntu-24.04
strategy:
matrix:
service: [order, stock, shipping, billing]
permissions:
contents: read
id-token: write # signature sans clé : jeton OIDC pour Sigstore
env:
IMAGE: registry.example.com/shop/${{ matrix.service }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: registry.example.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: services/${{ matrix.service }}/Dockerfile
push: true
tags: ${{ env.IMAGE }}:${{ github.sha }}
- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- run: sh tools/install-syft.sh
- name: SBOM, signature et attestation du digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
./syft scan "registry:${IMAGE}@${DIGEST}" -o cyclonedx-json=sbom.cdx.json
cosign sign --yes "${IMAGE}@${DIGEST}"
cosign attest --yes --type cyclonedx --predicate sbom.cdx.json "${IMAGE}@${DIGEST}"
Nous signons le digest, jamais le tag, comme le demande
cosign : un tag peut changer de cible entre le build et la
signature. L'action cosign-installer vérifie
l'intégrité du binaire qu'elle installe. Trivy produit un
document équivalent avec
trivy image --format cyclonedx --output sbom.cdx.json
"${IMAGE}@${DIGEST}".
7. Vérifier au déploiement, puis rescanner
Une signature que personne ne vérifie n'apporte rien. Avant d'appliquer un nouveau digest, l'agent de déploiement de l'environnement cible contrôle la signature et l'attestation :
# Dans l'environnement cible, avant le déploiement du digest
cosign verify-attestation --type cyclonedx \
--certificate-identity "https://github.com/ORG/REPO/.github/workflows/images.yml@refs/heads/main" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"registry.example.com/shop/order@${DIGEST}"
cosign verify, avec les mêmes options, contrôle
la signature de l'image. En zone isolée, fournissez à cosign
la racine de confiance de Sigstore
(--trusted-root). Sur Kubernetes, un contrôleur
d'admission peut imposer cette règle à tout le cluster.
Le SBOM sert encore après la livraison. Une tâche planifiée
relit avec trivy sbom le SBOM extrait des
attestations de l'étape 7 : une CVE publiée ce matin remonte
sans reconstruire l'image.
Checklist
-
Chaque
uses:pointe vers un SHA de commit complet, avec la version en commentaire. -
La règle d'épinglage par SHA est activée, et le
GITHUB_TOKENest en lecture seule par défaut. -
Chaque
FROMet chaque image de service de test portent un digest. - Chaque outil téléchargé est vérifié par somme de contrôle.
- Les actions composites retenues ont été lues, dépendances comprises.
- Dependabot ou Renovate couvre actions, images, Composer et npm, avec une cadence tenue.
- Composer et npm passent par un proxy ; la CI installe depuis les lockfiles.
-
composer auditetnpm audittournent à chaque build, sans cache. - Chaque image est signée et porte une attestation SBOM, sur son digest.
- Le déploiement refuse une image non signée ou sans attestation.
- Les SBOM des images en production sont rescannés régulièrement.
Sources
- Incident Trivy : avis GHSA-69fq-xp46-6x23, fiche CVE-2026-33634, annonce d'Aqua Security.
- GitHub : Secure use reference, règle d'épinglage par SHA, feuille de route sécurité 2026, gh-actions-lock, réglages Actions d'un dépôt.
-
Dependabot :
options
(
directories,groups,cooldown), commentaires de version, images des workflows ; Renovate, GitHub Actions. - Docker, Pin base image versions.
- Formats et outils : CycloneDX, SPDX, Syft, Trivy.
- Sigstore : signer des conteneurs, signature sans clé, cosign attest, cosign-installer.
- Composer : audit, policy ; npm : audit, ci.
- Proxys : Nexus Repository, Artifactory, Verdaccio, Private Packagist.
- Autres plateformes : GitLab CI/CD components, tâches Azure Pipelines, configuration de proto.