Aller au contenu
CI/CD & Automatisation medium

GitLab CI : sécuriser la supply chain avec SBOM et attestations

16 min de lecture

Un pipeline qui construit une image Docker sans la signer ni documenter ses composants ne fournit aucune garantie d’intégrité. Un attaquant qui compromet le registre, le runner ou un include peut remplacer l’artefact silencieusement. Ce guide vous montre comment ajouter trois couches de protection — SBOM, signature, attestation — dans vos pipelines GitLab CI, avec des outils open source que vous pouvez tester immédiatement sur votre instance KVM.

À la fin de ce guide, vous saurez :

  • Générer un SBOM (Software Bill of Materials) au format SPDX ou CycloneDX avec Syft et Trivy
  • Signer une image OCI avec cosign (Sigstore) et stocker la signature dans le registre
  • Produire une attestation SLSA de provenance rattachée à l’image
  • Vérifier la signature et l’attestation dans un pipeline de déploiement
  • Exploiter le SBOM pour détecter les vulnérabilités connues

La sécurité supply chain CI/CD n’est pas un exercice théorique. Vous en avez besoin quand :

  • Vous déployez des images Docker en production et devez prouver leur intégrité à un auditeur ou un client
  • Votre organisation doit se conformer à SLSA Level 2+, au SSDF du NIST ou à la directive NIS2
  • Vous utilisez des images de base tierces et devez savoir exactement quels composants elles embarquent
  • Un incident supply chain (dépendance compromise, registre altéré) vous oblige à tracer ce qui a été déployé
  • Vous gérez un registre interne et voulez bloquer les images non signées

La sécurité supply chain repose sur trois mécanismes complémentaires :

CoucheQuestionOutilFormat
SBOMQuels composants contient mon image ?Syft, TrivySPDX, CycloneDX
SignatureL’image a-t-elle été modifiée ?cosign (Sigstore)Signature OCI
AttestationQui l’a buildée, quand, comment ?cosign attestIn-toto / SLSA

Chaque couche répond à une question différente. Un SBOM sans signature ne prouve rien. Une signature sans attestation ne dit pas d’où vient l’artefact. Les trois ensemble forment une chaîne de confiance vérifiable.

Syft (Anchore) analyse une image et produit un SBOM détaillé :

.gitlab-ci.yml
sbom-syft:
stage: test
image:
name: anchore/syft:latest
entrypoint: [""]
script:
- syft $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -o spdx-json=sbom-spdx.json
- syft $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -o cyclonedx-json=sbom-cdx.json
artifacts:
paths:
- sbom-spdx.json
- sbom-cdx.json
expire_in: 90 days

Le SBOM liste chaque paquet, bibliothèque et dépendance avec sa version exacte.

Trivy peut produire un SBOM et scanner les vulnérabilités en un seul passage :

.gitlab-ci.yml
sbom-trivy:
stage: test
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --format spdx-json --output sbom-trivy.json
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- trivy sbom sbom-trivy.json --severity HIGH,CRITICAL --exit-code 1
artifacts:
paths:
- sbom-trivy.json
expire_in: 90 days

La deuxième commande utilise le SBOM comme entrée pour détecter les CVE. Si une vulnérabilité HIGH ou CRITICAL est trouvée, le job échoue.

Fenêtre de terminal
# Compter les composants
jq '.packages | length' sbom-spdx.json
# Lister les composants avec leur version
jq -r '.packages[] | "\(.name) \(.versionInfo)"' sbom-spdx.json | head -20
# Chercher un paquet spécifique
jq '.packages[] | select(.name | contains("openssl"))' sbom-spdx.json

Résultat attendu :

142
alpine-baselayout 3.4.3-r2
alpine-keys 2.4-r1
apk-tools 2.14.0-r5
busybox 1.36.1-r15
...

cosign fait partie du projet Sigstore. Il signe des artefacts OCI et stocke la signature directement dans le registre, à côté de l’image :

.gitlab-ci.yml
sign-image:
stage: deploy
image: alpine:3.20
before_script:
- apk add --no-cache cosign
- echo "$COSIGN_PRIVATE_KEY" > /tmp/cosign.key
script:
- cosign sign --key /tmp/cosign.key
--yes
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
after_script:
- rm -f /tmp/cosign.key
variables:
COSIGN_PASSWORD: $COSIGN_KEY_PASSWORD

Sur votre poste (une seule fois) :

Fenêtre de terminal
cosign generate-key-pair

Résultat :

Enter password for private key:
Private key written to cosign.key
Public key written to cosign.pub

Stockez cosign.key dans une variable CI protégée et masquée. Versionnez cosign.pub dans le dépôt pour la vérification.

Le mode keyless élimine la gestion des clés. cosign obtient un certificat éphémère via Fulcio, authentifié par le token OIDC de GitLab :

.gitlab-ci.yml
sign-keyless:
stage: deploy
image: alpine:3.20
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
before_script:
- apk add --no-cache cosign
script:
- cosign sign
--yes
--fulcio-url=https://fulcio.sigstore.dev
--rekor-url=https://rekor.sigstore.dev
--oidc-issuer=$CI_SERVER_URL
--identity-token=$SIGSTORE_ID_TOKEN
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
Fenêtre de terminal
# Avec clé
cosign verify --key cosign.pub $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# Keyless — vérifier l'identité du pipeline
cosign verify \
--certificate-identity-regexp="https://gitlab.example.com/my-group/my-project//.*" \
--certificate-oidc-issuer=https://gitlab.example.com \
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

Résultat attendu :

Verification for registry.example.com/my-group/my-project:abc1234 --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key

Si la vérification échoue, la sortie affiche Error: no matching signatures.

Une attestation de provenance lie l’artefact à son contexte de build : qui, quand, comment, avec quoi.

.gitlab-ci.yml
attest-sbom:
stage: deploy
image: alpine:3.20
needs:
- sbom-syft
- sign-image
before_script:
- apk add --no-cache cosign
- echo "$COSIGN_PRIVATE_KEY" > /tmp/cosign.key
script:
- cosign attest --key /tmp/cosign.key
--yes
--predicate sbom-spdx.json
--type spdxjson
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
after_script:
- rm -f /tmp/cosign.key
variables:
COSIGN_PASSWORD: $COSIGN_KEY_PASSWORD

Pour une attestation conforme SLSA, utilisez le prédicat slsaprovenance :

.gitlab-ci.yml
attest-provenance:
stage: deploy
image: alpine:3.20
before_script:
- apk add --no-cache cosign jq
- echo "$COSIGN_PRIVATE_KEY" > /tmp/cosign.key
script:
- |
cat > provenance.json <<EOF
{
"buildType": "https://gitlab.com/gitlab-ci",
"builder": {
"id": "$CI_RUNNER_ID"
},
"invocation": {
"configSource": {
"uri": "$CI_PROJECT_URL",
"digest": {"sha1": "$CI_COMMIT_SHA"},
"entryPoint": ".gitlab-ci.yml"
}
},
"metadata": {
"buildStartedOn": "$CI_JOB_STARTED_AT",
"completeness": {
"parameters": true,
"environment": true,
"materials": false
}
},
"materials": [
{
"uri": "$CI_REGISTRY_IMAGE",
"digest": {"sha256": "$(crane digest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA | cut -d: -f2)"}
}
]
}
EOF
- cosign attest --key /tmp/cosign.key
--yes
--predicate provenance.json
--type slsaprovenance
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
after_script:
- rm -f /tmp/cosign.key
variables:
COSIGN_PASSWORD: $COSIGN_KEY_PASSWORD
Fenêtre de terminal
# Vérifier l'attestation SBOM
cosign verify-attestation --key cosign.pub \
--type spdxjson \
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA | jq '.payload' | base64 -d | jq .
# Vérifier l'attestation de provenance
cosign verify-attestation --key cosign.pub \
--type slsaprovenance \
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA | jq '.payload' | base64 -d | jq .

Résultat attendu (provenance) :

{
"buildType": "https://gitlab.com/gitlab-ci",
"builder": {
"id": "42"
},
"invocation": {
"configSource": {
"uri": "https://gitlab.example.com/my-group/my-project",
"digest": {"sha1": "a1b2c3d4..."},
"entryPoint": ".gitlab-ci.yml"
}
}
}

Pipeline complet : build → SBOM → sign → attest → verify

Section intitulée « Pipeline complet : build → SBOM → sign → attest → verify »

Voici un pipeline qui enchaîne toutes les étapes :

.gitlab-ci.yml
stages:
- build
- test
- sign
- verify
variables:
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
build:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
script:
- /kaniko/executor
--context $CI_PROJECT_DIR
--dockerfile Dockerfile
--destination $IMAGE
--digest-file /tmp/digest
artifacts:
paths:
- /tmp/digest
sbom:
stage: test
image:
name: anchore/syft:latest
entrypoint: [""]
script:
- syft $IMAGE -o spdx-json=sbom.json
artifacts:
paths:
- sbom.json
vuln-scan:
stage: test
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy sbom sbom.json --severity HIGH,CRITICAL --exit-code 1
needs:
- sbom
sign-and-attest:
stage: sign
image: alpine:3.20
needs:
- build
- sbom
before_script:
- apk add --no-cache cosign
- echo "$COSIGN_PRIVATE_KEY" > /tmp/cosign.key
script:
- cosign sign --key /tmp/cosign.key --yes $IMAGE
- cosign attest --key /tmp/cosign.key --yes
--predicate sbom.json --type spdxjson $IMAGE
after_script:
- rm -f /tmp/cosign.key
variables:
COSIGN_PASSWORD: $COSIGN_KEY_PASSWORD
verify-before-deploy:
stage: verify
image: alpine:3.20
before_script:
- apk add --no-cache cosign
script:
- cosign verify --key cosign.pub $IMAGE
- cosign verify-attestation --key cosign.pub --type spdxjson $IMAGE
allow_failure: false

Pour empêcher le déploiement d’images non vérifiées, ajoutez un job de vérification obligatoire dans le pipeline de déploiement :

.gitlab-ci.yml — pipeline de déploiement
verify-signature:
stage: pre-deploy
image: alpine:3.20
before_script:
- apk add --no-cache cosign
script:
- |
if ! cosign verify --key cosign.pub $DEPLOY_IMAGE 2>/dev/null; then
echo "ERREUR : image non signée ou signature invalide"
echo "Image : $DEPLOY_IMAGE"
exit 1
fi
echo "Signature vérifiée"
allow_failure: false

Pour Kubernetes, Kyverno et Connaisseur peuvent bloquer les images non signées au niveau du cluster.

SymptômeCause probableCorrection
Error: no matching signaturesImage non signée ou mauvaise cléVérifier que cosign.pub correspond à la clé utilisée pour signer
UNAUTHORIZED: authentication requireddocker login non faitAjouter cosign login au before_script avec CI_REGISTRY_*
Error: signing [image]: getting remote imageImage introuvableVérifier que le tag $CI_COMMIT_SHORT_SHA existe dans le registre
SBOM vide (0 packages)Image scratch ou distrolessSyft ne peut pas scanner un filesystem vide — scanner le Dockerfile ou les sources
MANIFEST_UNKNOWN sur l’attestationLe registre ne supporte pas OCI artifactsMettre à jour le registre ou utiliser --attachment-tag-prefix
cosign keyless échoueOIDC mal configuréVérifier id_tokens dans le .gitlab-ci.yml et /.well-known/openid-configuration
Trivy SBOM scan timeoutImage volumineuseAjouter --timeout 15m et utiliser le cache Trivy DB

La sécurité supply chain CI repose sur trois couches complémentaires :

  1. SBOM : documenter ce que contient chaque image. Syft ou Trivy génèrent un inventaire complet au format standard (SPDX, CycloneDX). Sans SBOM, vous ne pouvez pas répondre à la question “est-ce que ce CVE nous concerne ?”
  2. Signature : prouver l’intégrité. cosign signe l’image et stocke la signature dans le registre. Toute modification invalide la signature. Le mode keyless avec Sigstore élimine la gestion des clés.
  3. Attestation : prouver la provenance. L’attestation SLSA lie l’artefact au pipeline qui l’a construit. Sans attestation, une signature prouve l’intégrité mais pas l’origine.
  4. Vérification : le maillon critique. Signer sans vérifier est inutile. Le job verify-before-deploy doit être obligatoire et bloquant dans le pipeline de déploiement.
  5. Automatisation : chaque étape doit être dans le pipeline, pas dans un script manuel. Ce qui n’est pas automatisé ne sera pas fait.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

10 questions
5 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn