in-toto : attestations pour la supply chain logicielle
Mise à jour :
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.
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 saurez faire à la fin
- 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
- 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 ?
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.
Analogie : le carnet de route d’un colis
Pensez à un colis expédié de l’autre côté du monde. À chaque étape (entrepôt, douane, centre de tri, livreur), quelqu’un scanne le colis et enregistre son passage. À la réception, vous pouvez vérifier que le colis a bien suivi l’itinéraire prévu et qu’il n’a pas été ouvert en route.
in-toto fait la même chose pour votre code : chaque étape du pipeline “scanne” les fichiers et signe un reçu. À la fin, vous vérifiez que tout s’est passé comme prévu.
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
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)
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
À 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
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
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
name: Build and attest container
on: push: tags: ['v*']
permissions: contents: read
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
- 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 }}
- name: Export digest run: | echo "digest=${{ steps.push.outputs.digest }}" >> $GITHUB_OUTPUT
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
-
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
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
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
L’outil slsa-verifier vérifie que l’attestation correspond à vos exigences :
# Installer slsa-verifiergo install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest
# 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
Cosign peut également vérifier les attestations in-toto :
# Vérifier que l'image a une attestation SLSAcosign verify-attestation ghcr.io/user/myapp@sha256:abc123... \ --type slsaprovenance \ --certificate-identity-regexp="^https://github.com/slsa-framework/slsa-github-generator/" \ --certificate-oidc-issuer=https://token.actions.githubusercontent.comExtraire 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
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
Kyverno est un policy engine Kubernetes qui peut vérifier les attestations avant d’autoriser le déploiement :
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: verify-slsa-provenancespec: validationFailureAction: Enforce webhookTimeoutSeconds: 30 rules: - name: check-slsa-attestation match: any: - resources: kinds: - Pod verifyImages: - imageReferences: - "ghcr.io/myorg/*" attestations: - predicateType: https://slsa.dev/provenance/v1 attestors: - entries: - keyless: issuer: https://token.actions.githubusercontent.com subjectRegExp: "^https://github.com/slsa-framework/slsa-github-generator/" conditions: - all: - key: "{{ buildDefinition.externalParameters.workflow.repository }}" operator: Equals value: "https://github.com/myorg/*"Cette policy :
- S’applique à toutes les images
ghcr.io/myorg/* - Exige une attestation SLSA provenance v1
- Vérifie que l’attestation vient bien de
slsa-github-generator - Vérifie que le build provient d’un repository de l’organisation
myorg
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)
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
name: Release Go binary with SLSA
on: push: tags: ['v*']
permissions: contents: read
jobs: build: runs-on: ubuntu-24.04 outputs: hashes: ${{ steps.hash.outputs.hashes }}
steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- 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
# 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
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
-
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
| 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
-
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.