Aller au contenu

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.4 et latest de 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

  1. Le job obtient un jeton OIDC émis par GitLab pour l'audience sigstore
  2. Cosign génère une paire de clés éphémère en mémoire
  3. Fulcio vérifie le jeton et délivre un certificat qui lie la clé publique à l'identité de la pipeline
  4. Cosign signe le digest de l'image
  5. La signature et le certificat sont consignés dans Rekor
  6. 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-regexp existe, 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, attest et verify-attestation restent 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 dans cosign sign --help de 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_PASSWORD vivent 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=max et --opt attest:sbom= attachent des attestations à l'image, non signées par Sigstore
  • GitLab Runner : RUNNER_GENERATE_ARTIFACTS_METADATA produit une provenance pour les artefacts de job
  • Cosign : cosign attest --type slsaprovenance1 pour 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 main du 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 main du 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 main ou 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/