Vous signez vos images Docker avec Cosign. Vous générez des SBOM avec Syft. Pourtant, une question reste sans réponse : comment prouver que votre pipeline CI/CD a bien exécuté toutes les étapes prévues ?
Imaginez ce scénario : un attaquant compromet votre pipeline et désactive silencieusement l'étape de scan de vulnérabilités. L'image est toujours signée, le SBOM est généré, mais le scan n'a jamais eu lieu. Sans trace de ce qui s'est réellement passé pendant le build, impossible de détecter la manipulation.
Prouver l'intégrité de chaque étape du build est précisément ce qu'exige le référentiel Socle DevSecOps au titre de la provenance et de l'intégrité du pipeline.
in-toto résout ce problème. Ce framework crée des attestations cryptographiquement signées pour chaque étape de votre pipeline. À la fin, vous pouvez prouver non seulement qui a construit l'artefact, mais aussi comment il a été construit, étape par étape.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre le modèle d'attestations in-toto (layout, links, vérification)
- Générer des attestations SLSA avec GitHub Actions
- Vérifier qu'un artefact a bien suivi le pipeline attendu
- Intégrer in-toto avec Cosign et l'écosystème Sigstore
Prérequis
Section intitulée « Prérequis »- Connaître les bases de la signature d'artefacts, voir Cosign
- Comprendre le framework SLSA, voir SLSA
- Un compte GitHub pour les exemples pratiques
Qu'est-ce qu'in-toto ?
Section intitulée « Qu'est-ce qu'in-toto ? »in-toto (du latin "dans son ensemble") est un framework qui garantit l'intégrité de bout en bout d'une chaîne de production logicielle.
L'idée est simple : chaque étape du pipeline (clone, build, test, package) génère une attestation, un document signé qui certifie :
- Quels fichiers étaient présents en entrée (les "matériaux")
- Quels fichiers ont été produits en sortie (les "produits")
- Qui a exécuté l'étape (quelle clé a signé)
- Quand l'étape s'est exécutée
À la fin du pipeline, un vérificateur compare ces attestations à une politique (le "layout") qui définit les étapes requises. Si une étape manque, si les hachages ne correspondent pas, ou si une signature est invalide, la vérification échoue.
Pourquoi in-toto est nécessaire
Section intitulée « Pourquoi in-toto est nécessaire »Une signature d'image Docker (avec Cosign) prouve qui a signé. Mais elle ne prouve pas :
| Question | Signature seule | Avec in-toto |
|---|---|---|
| Qui a signé l'artefact ? | ✅ Oui | ✅ Oui |
| Le code source a-t-il été scanné ? | ❌ Non | ✅ Oui |
| Les tests ont-ils été exécutés ? | ❌ Non | ✅ Oui |
| Le build vient-il du bon commit ? | ❌ Non | ✅ Oui |
| Le pipeline a-t-il été contourné ? | ❌ Non | ✅ Oui |
in-toto comble ce manque en fournissant une traçabilité complète du pipeline.
Les trois concepts clés
Section intitulée « Les trois concepts clés »1. Layout (la politique)
Section intitulée « 1. Layout (la politique) »Le layout définit les règles du jeu. C'est un document JSON signé par le propriétaire du projet qui spécifie :
- Quelles étapes sont requises (clone, build, test, sign...)
- Qui peut exécuter chaque étape (quelles clés sont autorisées)
- Quels fichiers doivent être présents en entrée/sortie
{ "_type": "https://in-toto.io/Layout/v1", "expires": "2026-01-01T00:00:00Z", "keys": { "builder-key-id": { "keytype": "ecdsa", "keyval": { "public": "..." } } }, "steps": [ { "name": "clone", "expected_command": ["git", "clone"], "pubkeys": ["builder-key-id"], "expected_materials": [], "expected_products": [["MATCH", "src/*", "WITH", "PRODUCTS", "FROM", "clone"]] }, { "name": "build", "expected_command": ["make", "build"], "pubkeys": ["builder-key-id"], "expected_materials": [["MATCH", "src/*", "WITH", "PRODUCTS", "FROM", "clone"]], "expected_products": [["CREATE", "dist/app"]] }, { "name": "test", "expected_command": ["make", "test"], "pubkeys": ["builder-key-id"], "expected_materials": [["MATCH", "dist/app", "WITH", "PRODUCTS", "FROM", "build"]], "expected_products": [] } ], "inspect": []}Le layout est la source de vérité. Un attaquant qui modifie le pipeline doit aussi compromettre le layout, ce qui nécessite de voler la clé du propriétaire du projet.
2. Link (l'attestation)
Section intitulée « 2. Link (l'attestation) »Un link est l'attestation générée après l'exécution d'une étape. Il contient les hachages SHA-256 des fichiers en entrée et en sortie :
{ "_type": "https://in-toto.io/Link/v1", "name": "build", "materials": { "src/main.go": { "sha256": "a1b2c3..." }, "src/utils.go": { "sha256": "d4e5f6..." } }, "products": { "dist/app": { "sha256": "789abc..." } }, "byproducts": { "return-value": 0, "stdout": "Build successful" }, "command": ["make", "build"], "environment": {}}Chaque link est signé par l'entité qui a exécuté l'étape (le runner CI, un développeur, un système de build).
3. Vérification
Section intitulée « 3. Vérification »À la fin du pipeline, un vérificateur compare les links au layout :
-
Vérifier les signatures : chaque link doit être signé par une clé autorisée dans le layout.
-
Vérifier la chaîne : les "products" de l'étape N doivent correspondre aux "materials" de l'étape N+1 (les hachages doivent être identiques).
-
Vérifier la complétude : toutes les étapes définies dans le layout doivent avoir un link correspondant.
Si une seule vérification échoue, l'artefact est rejeté.
in-toto et SLSA : le lien
Section intitulée « in-toto et SLSA : le lien »SLSA (Supply-chain Levels for Software Artifacts) définit des niveaux de maturité pour la sécurité supply chain. in-toto est le format d'attestation recommandé par SLSA pour atteindre les niveaux 2 et supérieurs.
| Niveau SLSA | Exigence | Rôle in-toto |
|---|---|---|
| Build L1 | Provenance générée | Attestation de build basique |
| Build L2 | Provenance signée par la plateforme | Signature cryptographique du link |
| Build L3 | Build isolé et durci | Layout avec contraintes strictes |
Générer des attestations avec GitHub Actions
Section intitulée « Générer des attestations avec GitHub Actions »GitHub Actions permet de générer des attestations SLSA conformes au format
in-toto via le projet officiel slsa-github-generator. Voici comment
l'implémenter pour une image Docker.
Workflow complet
Section intitulée « Workflow complet »Deux jobs séparés, et cette séparation n'est pas cosmétique : le job build
n'a jamais le droit id-token: write, c'est le job provenance qui le
porte. Un attaquant qui prend la main sur l'étape de build ne peut donc pas
demander un jeton OIDC à GitHub et signer ce qu'il veut. Le seul lien entre
les deux jobs est le digest de l'image, une valeur qui ne peut pas être
falsifiée sans changer l'image elle-même. Notez aussi le permissions: {} au
niveau du workflow : il coupe tous les droits par défaut, chaque job ne
redemande que ce dont il a besoin.
name: Build and attest container
on: push: tags: ['v*']
permissions: {}
env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}
jobs: build: runs-on: ubuntu-24.04 outputs: digest: ${{ steps.push.outputs.digest }} permissions: contents: read packages: write
steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false
- name: Log in to Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image id: push uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
provenance: needs: build permissions: actions: read id-token: write packages: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 with: image: ghcr.io/${{ github.repository }} digest: ${{ needs.build.outputs.digest }} registry-username: ${{ github.actor }} secrets: registry-password: ${{ secrets.GITHUB_TOKEN }}Ce que fait ce workflow
Section intitulée « Ce que fait ce workflow »-
Build : construit l'image Docker et la pousse vers le registry GitHub (ghcr.io). Le digest SHA-256 est capturé en sortie.
-
Provenance : appelle le workflow réutilisable de slsa-framework qui génère une attestation in-toto signée par GitHub Actions lui-même (via OIDC).
-
Stockage : l'attestation est attachée à l'image via les signatures OCI et enregistrée dans le log de transparence Rekor.
Contenu de l'attestation générée
Section intitulée « Contenu de l'attestation générée »L'attestation SLSA générée est un document in-toto avec un "predicate" de type
https://slsa.dev/provenance/v1. Voici un extrait simplifié :
{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "ghcr.io/user/myapp", "digest": { "sha256": "abc123..." } } ], "predicateType": "https://slsa.dev/provenance/v1", "predicate": { "buildDefinition": { "buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1", "externalParameters": { "workflow": { "ref": "refs/tags/v1.0.0", "repository": "https://github.com/user/myapp" } } }, "runDetails": { "builder": { "id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.1.0" }, "metadata": { "invocationId": "https://github.com/user/myapp/actions/runs/12345" } } }}Cette attestation prouve que :
- L'image
ghcr.io/user/myapp@sha256:abc123...a été construite - Par le workflow GitHub Actions spécifié
- À partir du tag
v1.0.0du repositoryuser/myapp - Via le builder officiel
slsa-github-generatorv2.1.0
Vérifier une attestation
Section intitulée « Vérifier une attestation »Générer des attestations ne sert à rien si personne ne les vérifie. Voici comment vérifier qu'une image a bien été construite selon les attentes.
Avec slsa-verifier
Section intitulée « Avec slsa-verifier »L'outil slsa-verifier vérifie que l'attestation correspond à vos exigences :
# Installer slsa-verifier (version épinglée, pas @latest)go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@v2.7.1
# Vérifier une image Dockerslsa-verifier verify-image ghcr.io/user/myapp@sha256:abc123... \ --source-uri github.com/user/myapp \ --source-tag v1.0.0Résultat attendu :
Verified signature against tlog entry index 12345678 at URL: https://rekor.sigstore.devVerified build using builder "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.1.0"Verifying artifact ghcr.io/user/myapp@sha256:abc123...PASSED: SLSA verification passedAvec Cosign
Section intitulée « Avec Cosign »Cosign peut également vérifier les attestations in-toto :
Attention au raccourci passé à --type : slsaprovenance désigne le predicate
SLSA v0.2, alors que slsa-github-generator v2 produit du
https://slsa.dev/provenance/v1. Le raccourci à utiliser est donc
slsaprovenance1, sinon la vérification rejette une attestation pourtant
valide.
# Vérifier que l'image a une attestation SLSA v1cosign verify-attestation ghcr.io/user/myapp@sha256:abc123... \ --type slsaprovenance1 \ --certificate-identity-regexp="^https://github.com/slsa-framework/slsa-github-generator/" \ --certificate-oidc-issuer=https://token.actions.githubusercontent.comExtraire et inspecter l'attestation
Section intitulée « Extraire et inspecter l'attestation »Pour voir le contenu brut de l'attestation :
# Télécharger l'attestationcosign download attestation ghcr.io/user/myapp@sha256:abc123... > attestation.json
# Décoder le payload (base64)cat attestation.json | jq -r '.payload' | base64 -d | jq .Vérification automatique dans Kubernetes
Section intitulée « Vérification automatique dans Kubernetes »La génération et la vérification manuelle, c'est bien pour les tests. En production, vous voulez bloquer automatiquement les images sans attestation valide.
Avec Kyverno
Section intitulée « Avec Kyverno »Kyverno est un policy engine Kubernetes qui peut vérifier les attestations avant d'autoriser le déploiement :
apiVersion: policies.kyverno.io/v1kind: ImageValidatingPolicymetadata: name: verify-slsa-provenancespec: validationActions: [Deny] matchConstraints: resourceRules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["pods"] matchImageReferences: - glob: "ghcr.io/myorg/*" attestors: - name: slsaKeyless cosign: keyless: identities: - issuer: "https://token.actions.githubusercontent.com" subjectRegExp: "^https://github.com/slsa-framework/slsa-github-generator/" attestations: - name: slsaProvenance intoto: type: https://slsa.dev/provenance/v1 validations: - expression: >- images.containers.map(image, verifyAttestationSignatures(image, attestations.slsaProvenance, [attestors.slsaKeyless])).all(e, e > 0) message: "Image must have a valid SLSA provenance attestation."Le point à ne pas rater est la dernière section. Une ImageValidatingPolicy ne
vérifie rien tant que le champ validations ne contient pas une expression
CEL qui appelle verifyAttestationSignatures. Écrire expression: "true"
donne une policy qui s'installe sans erreur, se déclare conforme, et laisse
passer toutes les images : le piège classique.
Une fois l'expression en place, la policy :
- S'applique à toutes les images
ghcr.io/myorg/* - Exige une attestation dont le predicate est
https://slsa.dev/provenance/v1 - Vérifie que la signature de cette attestation provient bien de
slsa-github-generator, via une identité keyless GitHub Actions
Avec Sigstore Policy Controller
Section intitulée « Avec Sigstore Policy Controller »Alternative à Kyverno, le Policy Controller de Sigstore est spécialisé dans la vérification de signatures et attestations :
apiVersion: policy.sigstore.dev/v1beta1kind: ClusterImagePolicymetadata: name: slsa-provenance-requiredspec: images: - glob: "ghcr.io/myorg/**" authorities: - name: slsa-generator keyless: url: https://fulcio.sigstore.dev identities: - issuerRegExp: "^https://token.actions.githubusercontent.com$" subjectRegExp: "^https://github.com/slsa-framework/slsa-github-generator/" attestations: - name: slsa-provenance predicateType: https://slsa.dev/provenance/v1Attestations pour binaires (hors conteneurs)
Section intitulée « Attestations pour binaires (hors conteneurs) »in-toto ne se limite pas aux images Docker. Vous pouvez attester n'importe quel artefact : binaires Go, packages npm, archives tar...
Exemple : binaire Go avec attestation
Section intitulée « Exemple : binaire Go avec attestation »La différence avec le workflow conteneur tient à ce qui identifie l'artefact.
Une image OCI porte son propre digest, le registry le calcule. Un binaire
n'a rien de tel : c'est au job de build de produire les empreintes SHA-256
et de les transmettre. C'est le rôle de l'étape Generate hashes, dont la
sortie hashes est encodée en base64 parce que le workflow réutilisable
generator_generic_slsa3.yml attend une liste de sujets sous cette forme. Si
vous modifiez la ligne de compilation sans regénérer les empreintes,
l'attestation décrira des binaires qui n'existent plus et la vérification
échouera.
name: Release Go binary with SLSA
on: push: tags: ['v*']
permissions: {}
jobs: build: runs-on: ubuntu-24.04 outputs: hashes: ${{ steps.hash.outputs.hashes }} permissions: contents: read
steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: '1.23'
- name: Build binaries run: | GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 . GOOS=darwin GOARCH=amd64 go build -o myapp-darwin-amd64 . GOOS=windows GOARCH=amd64 go build -o myapp-windows-amd64.exe .
- name: Generate hashes id: hash run: | echo "hashes=$(sha256sum myapp-* | base64 -w0)" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: binaries path: myapp-*
provenance: needs: build permissions: actions: read id-token: write contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: base64-subjects: ${{ needs.build.outputs.hashes }} upload-assets: trueVérifier un binaire
Section intitulée « Vérifier un binaire »Le fichier .intoto.jsonl publié à côté du binaire contient l'attestation au
format JSON Lines, une ligne par sujet attesté. Contrairement au cas de
l'image, rien ne rattache automatiquement ce fichier au binaire : vous devez
passer les deux à slsa-verifier, qui recalcule l'empreinte du fichier local et
la compare à celle inscrite dans l'attestation. Les options --source-uri et
--source-tag ne sont pas décoratives non plus : sans elles, une attestation
authentique mais issue d'un autre dépôt passerait la vérification.
# Télécharger le binaire et son attestationgh release download v1.0.0 -R user/myapp
# Vérifier avec slsa-verifierslsa-verifier verify-artifact myapp-linux-amd64 \ --provenance-path myapp-linux-amd64.intoto.jsonl \ --source-uri github.com/user/myapp \ --source-tag v1.0.0Intégration avec l'écosystème Sigstore
Section intitulée « Intégration avec l'écosystème Sigstore »in-toto s'intègre étroitement avec les outils Sigstore pour la signature et la transparence :
| Composant | Rôle dans in-toto |
|---|---|
| Cosign | Signe les attestations, les attache aux images OCI, vérifie les signatures |
| Fulcio | Délivre des certificats éphémères pour la signature keyless (pas de clé à gérer) |
| Rekor | Stocke les attestations dans un log de transparence public (non-répudiation) |
Flux de signature keyless
Section intitulée « Flux de signature keyless »-
Authentification OIDC : le workflow GitHub s'authentifie auprès de Fulcio avec son token OIDC.
-
Certificat éphémère : Fulcio délivre un certificat X.509 lié à l'identité du workflow (valide 10 minutes).
-
Signature : l'attestation in-toto est signée avec la clé privée associée au certificat.
-
Enregistrement : la signature est envoyée à Rekor qui l'horodate et l'enregistre.
-
Destruction : la clé privée est détruite immédiatement après usage.
Ce flux élimine le problème de la gestion des clés : pas de secret à stocker, pas de rotation à planifier.
Dépannage
Section intitulée « Dépannage »La plupart des échecs de vérification ne signalent pas une attaque mais un
décalage entre ce qui a été attesté et ce qui est demandé : mauvaise URL de
dépôt, version du générateur qui a bougé entre le build et le contrôle,
attestation qui n'a jamais été poussée. Lisez le message d'erreur de
slsa-verifier avant tout : il nomme toujours le champ qui ne correspond pas.
Un cas mérite d'être connu à part, la dernière ligne du tableau : un
certificat Fulcio expiré est le fonctionnement normal, ces certificats ne
vivent que quelques minutes et c'est l'entrée Rekor horodatée qui fait foi.
| Symptôme | Cause probable | Solution |
|---|---|---|
FAILED: no matching attestations | Attestation non générée ou non attachée | Vérifier les logs du job provenance |
FAILED: source mismatch | Le --source-uri ne correspond pas | Utiliser l'URL exacte du repository |
FAILED: builder mismatch | Version de slsa-github-generator différente | Vérifier la version dans le workflow |
| Timeout Kyverno | Vérification trop longue | Augmenter webhookTimeoutSeconds |
certificate has expired | Certificat Fulcio expiré | Normal, la preuve Rekor reste valide |
À retenir
Section intitulée « À retenir »-
in-toto = traçabilité du pipeline : prouve non seulement qui a signé, mais comment l'artefact a été construit.
-
Layout + Links : le layout définit les règles, les links attestent l'exécution de chaque étape.
-
Standard SLSA : in-toto est le format d'attestation recommandé par SLSA pour les niveaux Build L2+.
-
GitHub Actions natif :
slsa-github-generatorgénère des attestations conformes sans configuration complexe. -
Vérification obligatoire : utilisez
slsa-verifier, Kyverno ou Sigstore Policy Controller pour bloquer les artefacts non attestés. -
Signature keyless : via Fulcio et Rekor, plus de clés à gérer.