Aller au contenu

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 :

QuestionSignature seuleAvec 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.

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 :

  1. Vérifier les signatures : chaque link doit être signé par une clé autorisée dans le layout.

  2. 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).

  3. 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 SLSAExigenceRôle in-toto
Build L1Provenance généréeAttestation de build basique
Build L2Provenance signée par la plateformeSignature cryptographique du link
Build L3Build isolé et durciLayout 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

.github/workflows/release-with-provenance.yml
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

  1. Build : construit l’image Docker et la pousse vers le registry GitHub (ghcr.io). Le digest SHA-256 est capturé en sortie.

  2. 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).

  3. 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.0 du repository user/myapp
  • Via le builder officiel slsa-github-generator v2.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 :

Terminal window
# Installer slsa-verifier
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest
# Vérifier une image Docker
slsa-verifier verify-image ghcr.io/user/myapp@sha256:abc123... \
--source-uri github.com/user/myapp \
--source-tag v1.0.0

Résultat attendu :

Verified signature against tlog entry index 12345678 at URL: https://rekor.sigstore.dev
Verified 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 passed

Avec Cosign

Cosign peut également vérifier les attestations in-toto :

Terminal window
# Vérifier que l'image a une attestation SLSA
cosign 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.com

Extraire et inspecter l’attestation

Pour voir le contenu brut de l’attestation :

Terminal window
# Télécharger l'attestation
cosign 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 :

kyverno-slsa-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-slsa-provenance
spec:
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 :

clusterimagepolicy.yaml
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: slsa-provenance-required
spec:
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/v1

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

.github/workflows/go-release-slsa.yml
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: true

Vérifier un binaire

Terminal window
# Télécharger le binaire et son attestation
gh release download v1.0.0 -R user/myapp
# Vérifier avec slsa-verifier
slsa-verifier verify-artifact myapp-linux-amd64 \
--provenance-path myapp-linux-amd64.intoto.jsonl \
--source-uri github.com/user/myapp \
--source-tag v1.0.0

Intégration avec l’écosystème Sigstore

in-toto s’intègre étroitement avec les outils Sigstore pour la signature et la transparence :

ComposantRôle dans in-toto
CosignSigne les attestations, les attache aux images OCI, vérifie les signatures
FulcioDélivre des certificats éphémères pour la signature keyless (pas de clé à gérer)
RekorStocke les attestations dans un log de transparence public (non-répudiation)

Flux de signature keyless

  1. Authentification OIDC : le workflow GitHub s’authentifie auprès de Fulcio avec son token OIDC.

  2. Certificat éphémère : Fulcio délivre un certificat X.509 lié à l’identité du workflow (valide 10 minutes).

  3. Signature : l’attestation in-toto est signée avec la clé privée associée au certificat.

  4. Enregistrement : la signature est envoyée à Rekor qui l’horodate et l’enregistre.

  5. 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ômeCause probableSolution
FAILED: no matching attestationsAttestation non générée ou non attachéeVérifier les logs du job provenance
FAILED: source mismatchLe --source-uri ne correspond pasUtiliser l’URL exacte du repository
FAILED: builder mismatchVersion de slsa-github-generator différenteVérifier la version dans le workflow
Timeout KyvernoVérification trop longueAugmenter webhookTimeoutSeconds
certificate has expiredCertificat Fulcio expiréNormal — la preuve Rekor reste valide

À retenir

  1. in-toto = traçabilité du pipeline : prouve non seulement qui a signé, mais comment l’artefact a été construit.

  2. Layout + Links : le layout définit les règles, les links attestent l’exécution de chaque étape.

  3. Standard SLSA : in-toto est le format d’attestation recommandé par SLSA pour les niveaux Build L2+.

  4. GitHub Actions natif : slsa-github-generator génère des attestations conformes sans configuration complexe.

  5. Vérification obligatoire : utilisez slsa-verifier, Kyverno ou Sigstore Policy Controller pour bloquer les artefacts non attestés.

  6. Signature keyless : via Fulcio et Rekor, plus de clés à gérer.

Liens utiles

Ressources externes