Sécuriser la supply chain : Cosign, attestations & SLSA
Objectifs pédagogiques¶
- Comprendre les attaques visant la supply chain logicielle et leur point commun
- Appliquer le principe build once, deploy many avec une référence d'image par digest
- Signer une image sans clé longue durée avec Cosign et l'identité OIDC de la pipeline
- Attacher et vérifier des attestations : SBOM et provenance
- Situer une pipeline sur l'échelle SLSA
- Promouvoir une image signée par Merge Request et la vérifier avant exécution
Pourquoi la supply chain¶
Qu'est-ce que la supply chain logicielle¶
Tout ce qui intervient entre le code et la production peut injecter du code malveillant
- Sources : dépôt, contributeurs, droits de merge
- Dépendances : bibliothèques directes et transitives, images de base
- Outillage : images de build, scanners, actions et templates CI
- Plateforme de build : runners, variables, jetons
- Distribution : registre, tags, miroirs
- Déploiement : dépôt GitOps, opérateur, contrôle d'admission
Des attaques bien réelles¶
- SolarWinds (2020) : l'environnement de build d'Orion est compromis, une mise à jour signée embarque la porte dérobée SUNBURST
- Codecov (2021) : le script Bash Uploader est modifié et exfiltre les variables d'environnement des pipelines clientes
- XZ Utils (2024, CVE-2024-3094) : une porte dérobée est introduite par un contributeur devenu mainteneur, cachée dans les archives de release
- tj-actions/changed-files (2025) : les tags d'une GitHub Action populaire sont redirigés vers un commit qui fuite les secrets de CI
- Trivy (mars 2026) : les tags
0.69.4etlatestde l'image Docker Hub sont écrasés par une image malveillante après un vol d'identifiants
Le point commun¶
- Une confiance implicite dans un nom, un tag ou une source
- Une intégrité jamais vérifiée entre la production de l'artefact et son exécution
- Des identifiants de publication volés ou trop larges
- Une détection tardive, souvent par hasard
La réponse : rendre l'artefact immuable, prouver son origine et vérifier cette preuve avant l'exécution
Intégrité de l'artefact¶
Build once, deploy many¶
- L'image est construite une seule fois, dans la pipeline
- Le même artefact est testé, scanné, signé puis promu d'environnement en environnement
- Aucun rebuild entre staging et production
- Ce qui a passé les Quality Gates est exactement ce qui s'exécute
Rebuilder pour la production, c'est déployer un artefact qui n'a jamais été contrôlé
Tag ou digest¶
registry.gitlab.com/acme/securepay-api:v1.4.0
-> tag : pointeur mutable, réassignable à tout moment
registry.gitlab.com/acme/securepay-api@sha256:4f1c...e9a2
-> digest : empreinte SHA-256 du manifeste, immuable
- Un tag peut être réécrit après les scans : c'est exactement ce qui s'est passé avec Trivy en mars 2026
- Un digest change dès qu'un seul octet du manifeste change
- On affiche des tags pour les humains, on déploie et on signe des digests
Récupérer le digest au moment du build¶
BuildKit écrit les métadonnées du build dans un fichier JSON
build:image:
stage: build
image:
name: moby/buildkit:v0.34.0-rootless
entrypoint: [""]
variables:
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
script:
- buildctl-daemonless.sh build --frontend dockerfile.v0
--local context=. --local dockerfile=.
--output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA,push=true
--metadata-file metadata.json
- |
IMAGE_DIGEST=$(grep -o '"containerimage.digest": *"[^"]*"' metadata.json | cut -d'"' -f4)
echo "IMAGE_DIGEST=${IMAGE_DIGEST}" > build.env
artifacts:
reports:
dotenv: build.env
Référencer le digest dans le chart Helm¶
# values.yaml
image:
repository: registry.gitlab.com/acme/securepay-api
digest: "sha256:4f1c...e9a2"
# templates/deployment.yaml
containers:
- name: api
image: "{{ .Values.image.repository }}@{{ .Values.image.digest }}"
- La promotion se résume à changer une ligne dans le dépôt GitOps
- Le diff de la Merge Request montre exactement quel artefact part en production
Sigstore & Cosign¶
Sigstore en trois briques¶
- Cosign : signe et vérifie les images et artefacts, stocke signatures et attestations dans le registre OCI
- Fulcio : autorité de certification qui délivre des certificats de courte durée (10 minutes) liés à une identité OIDC
- Rekor : journal de transparence en ajout seul, qui horodate et rend publique chaque signature
Projet open source de l'OpenSSF, utilisé notamment par Kubernetes, npm et PyPI
Signature keyless : le flux¶
- Le job obtient un jeton OIDC émis par GitLab pour l'audience
sigstore - Cosign génère une paire de clés éphémère en mémoire
- Fulcio vérifie le jeton et délivre un certificat qui lie la clé publique à l'identité de la pipeline
- Cosign signe le digest de l'image
- La signature et le certificat sont consignés dans Rekor
- La clé privée est détruite : aucune clé à stocker, à protéger ni à faire tourner
L'identité d'une pipeline GitLab¶
Le certificat porte l'identité du fichier de pipeline et de la référence Git
Identité : https://gitlab.com/<groupe>/<projet>//.gitlab-ci.yml@refs/heads/main
Émetteur : https://gitlab.com
- Le double slash sépare le chemin du projet et celui du fichier de pipeline
- La référence est incluse : une signature produite sur une branche de travail n'a pas l'identité de
main - Sur une instance GitLab auto-hébergée, l'instance publique de Fulcio ne la reconnaît pas : il faut une infrastructure Sigstore privée ou une paire de clés
Signer dans GitLab CI¶
sign:image:
stage: sign
image: alpine:3.22
needs:
- job: build:image
artifacts: true
id_tokens:
SIGSTORE_ID_TOKEN: # lu automatiquement par Cosign
aud: sigstore
before_script:
- apk add --no-cache cosign
- echo "$CI_REGISTRY_PASSWORD" | cosign login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
script:
- cosign sign --yes "$CI_REGISTRY_IMAGE@$IMAGE_DIGEST"
- Toujours signer le digest, jamais un tag
- GitLab exige Cosign 2.0.1 ou plus récent
Vérifier une signature¶
cosign verify \
--certificate-identity "https://gitlab.com/acme/securepay-api//.gitlab-ci.yml@refs/heads/main" \
--certificate-oidc-issuer "https://gitlab.com" \
"registry.gitlab.com/acme/securepay-api@sha256:4f1c...e9a2"
- Vérifier une signature sans contraindre l'identité ne prouve rien : n'importe qui peut signer avec son propre compte
--certificate-identity-regexpexiste, mais une expression trop large rouvre la porte- Une vérification réussie confirme l'émetteur, l'identité, l'inclusion dans Rekor et l'intégrité du digest
Cosign v2 ou v3¶
- Cosign v3 active par défaut le format bundle standard de Sigstore
- En v3, les signatures d'images sont stockées comme artefacts référents OCI 1.1 plutôt que sous un tag
sha256-<digest>.sig - Les commandes
sign,verify,attestetverify-attestationrestent les mêmes - Vérifiez que votre registre et votre contrôleur d'admission savent lire le format produit
- En cas d'incompatibilité, l'ancien format reste disponible via
--new-bundle-format=false, à confirmer danscosign sign --helpde la version épinglée
Et sans keyless¶
Une paire de clés reste possible : instance auto-hébergée, réseau isolé, contrainte réglementaire
cosign generate-key-pair # cosign.key (chiffrée) et cosign.pub
cosign sign --yes --key env://COSIGN_PRIVATE_KEY "$IMAGE@$IMAGE_DIGEST"
cosign verify --key cosign.pub "$IMAGE@$IMAGE_DIGEST"
- La clé privée et
COSIGN_PASSWORDvivent dans des variables CI protégées, jamais dans le dépôt - La rotation, la révocation et la distribution de la clé publique sont à votre charge
- Un KMS (
awskms://,gcpkms://,azurekms://,hashivault://) évite de manipuler la clé privée
Attestations & SLSA¶
Signature ou attestation¶
- Signature : « cet artefact a été produit par cette identité »
- Attestation : déclaration signée à propos de l'artefact
- Sujet : le digest de l'image
- Prédicat : le contenu de la déclaration (SBOM, provenance, résultat de scan)
- Format standard in-toto, enveloppé dans une signature DSSE
- Les attestations permettent de vérifier non seulement l'origine, mais aussi les propriétés de l'artefact
Attester le SBOM¶
# Générer le SBOM de l'image exacte
trivy image --format cyclonedx --output sbom.cdx.json "$IMAGE@$IMAGE_DIGEST"
# L'attacher à l'image sous forme d'attestation signée
cosign attest --yes --type cyclonedx --predicate sbom.cdx.json "$IMAGE@$IMAGE_DIGEST"
# Vérifier l'attestation et son signataire
cosign verify-attestation --type cyclonedx \
--certificate-identity "$IDENTITY" \
--certificate-oidc-issuer "https://gitlab.com" \
"$IMAGE@$IMAGE_DIGEST"
- Autres types utiles :
spdxjson,slsaprovenance1,openvex,vuln - Le SBOM attesté permet de savoir en quelques minutes si une nouvelle CVE touche la production
La provenance¶
La provenance décrit où, comment et à partir de quoi l'artefact a été construit
- Dépôt et commit source
- Plateforme et job de build
- Paramètres et dépendances du build
Sources possibles
- BuildKit :
--opt attest:provenance=mode=maxet--opt attest:sbom=attachent des attestations à l'image, non signées par Sigstore - GitLab Runner :
RUNNER_GENERATE_ARTIFACTS_METADATAproduit une provenance pour les artefacts de job - Cosign :
cosign attest --type slsaprovenance1pour signer une provenance générée par la pipeline
SLSA : la piste Build¶
Supply-chain Levels for Software Artifacts, framework de l'OpenSSF
| Niveau | Exigence principale | Ce que l'on gagne |
|---|---|---|
| L0 | Aucune | Rien |
| L1 | Une provenance existe | Savoir comment l'artefact a été construit |
| L2 | Build sur une plateforme hébergée, provenance signée par la plateforme | Empêcher la falsification après le build |
| L3 | Plateforme durcie : builds isolés, secrets de signature inaccessibles aux étapes du build | Empêcher la falsification pendant le build |
Situer une pipeline GitLab¶
- Runners hébergés, image signée par l'identité de la pipeline, provenance signée : objectif L2
- L3 suppose des garanties fortes d'isolation entre builds et de protection des secrets de signature
- Un runner partagé entre projets avec des privilèges larges éloigne du niveau visé
- SLSA est une échelle : l'important est de connaître son niveau réel et de progresser
Promotion & vérification¶
Promouvoir par Merge Request¶
promote:
stage: promote
image: alpine:3.22
needs:
- job: build:image
artifacts: true
- job: sign:image
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- apk add --no-cache git
- git clone "https://promote-bot:${GITOPS_PROMOTE_TOKEN}@gitlab.com/acme/securepay-platform.git" gitops
- cd gitops && git switch -c "promote/api-${CI_COMMIT_SHORT_SHA}"
- sed -i "s|^\( *digest:\).*|\1 \"${IMAGE_DIGEST}\"|" charts/securepay-api/values.yaml
- git -c user.name=securepay-ci -c [email protected] commit -am "Promotion ${IMAGE_DIGEST}"
- git push origin HEAD -o merge_request.create -o merge_request.target=main
-o merge_request.title="Promotion securepay-api ${CI_COMMIT_SHORT_SHA}"
Pourquoi une MR plutôt qu'un push direct¶
- La branche
maindu dépôt GitOps reste protégée : personne ne pousse directement, pas même la CI - La pipeline du dépôt GitOps valide le changement : lint, rendu Helm, tests de politiques
- Une personne relit et approuve la mise en production
- L'historique des MR devient le registre d'audit des déploiements
- Le jeton de promotion a une portée minimale : un seul projet,
write_repository, expiration courte, variable Masked et Protected
Fermer la boucle à l'admission¶
Une signature n'a de valeur que si elle est vérifiée avant l'exécution
- Le contrôleur d'admission du cluster vérifie la signature de chaque image
- Il contraint l'émetteur et l'identité : seule la pipeline de
maindu bon projet est acceptée - Il peut exiger une attestation : SBOM présent, provenance conforme
- Une image non signée, signée par une autre identité ou référencée par tag est refusée
Mise en œuvre avec Kyverno et verifyImages : voir le deck 04 de ce cours, consacré à la sécurisation du cluster Kubernetes
La chaîne complète¶
commit sur main
-> build BuildKit ............. digest sha256:...
-> scan Trivy + SBOM .......... échec si CVE CRITICAL
-> cosign sign + attest ....... identité OIDC de la pipeline, Rekor
-> MR de promotion ............ digest dans le dépôt GitOps, revue
-> ArgoCD ..................... synchronise l'état désiré
-> admission Kyverno .......... vérifie signature et identité
-> pod en production .......... exactement l'artefact contrôlé
Pièges fréquents¶
- Signer ou déployer un tag au lieu d'un digest
- Vérifier une signature sans contraindre l'identité ou avec une expression régulière trop large
- Accepter les signatures de toutes les branches au lieu de
mainou des tags de release - Utiliser des images d'outillage non épinglées dans les jobs de sécurité
- Donner au jeton de promotion des droits sur tout le groupe
- Signer un projet privé sans savoir que Rekor est public : le chemin du projet y apparaît
À retenir¶
- Les attaques supply chain exploitent la confiance implicite dans un tag, une source ou un outil
- On construit une fois, on promeut un digest immuable
- Cosign keyless lie la signature à l'identité de la pipeline, sans clé à gérer
- Les attestations signées transportent SBOM et provenance avec l'image
- SLSA donne une échelle pour mesurer et améliorer la chaîne de build
- La promotion passe par une Merge Request et la vérification se fait à l'admission
Pour aller plus loin¶
- Documentation Sigstore : https://docs.sigstore.dev/
- Spécification SLSA : https://slsa.dev/
- Signature keyless dans GitLab CI : https://docs.gitlab.com/ci/yaml/signing_examples/
- Analyse d'images avec Trivy : https://bastienmaurice.fr/slides/common/container-analysis-with-trivy/
- BuildKit et Buildx : https://bastienmaurice.fr/platform/modern-ci/buildx-and-buildkit/