
Le pipeline CI garantit que
le code est testé et durci. Reste à rendre l'image publiée vérifiable : prouver
d'où elle vient, ce qu'elle contient, et qu'elle n'a pas été substituée dans le
registre. Ce guide construit, pas à pas, le workflow release.yml du dépôt de
référence : build et push par digest sur GHCR, provenance SLSA native,
SBOM CycloneDX attesté et signature Cosign keyless. Il montre ensuite
comment un tiers vérifie toute la chaîne, et pourquoi une version de Cosign
mal alignée fait croire à tort qu'une release est cassée. La règle de fond : une
image sans provenance ni signature est indistinguable d'une image compromise.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Construire et pousser une image sur le registre GitHub (GHCR) par digest
- Générer une attestation de provenance SLSA avec l'action native GitHub
- Attester un SBOM CycloneDX et signer l'image avec Cosign en mode keyless
- Vérifier la provenance et la signature en tant que tiers, sans piège de version
Prérequis
Section intitulée « Prérequis »Le pipeline CI durci est en
place : c'est lui qui introduit le durcissement de base des workflows (épinglage
par SHA, permissions: {}, harden-runner, checkout sans credentials). Ce guide
ne le réexplique pas, il l'applique. Lecture conseillée en parallèle :
SLSA,
Sigstore et
SBOM. Côté machine, la CLI gh est
authentifiée et Docker est disponible pour les vérifications locales. Tout le code
montré ici vient du dépôt public de référence
github.com/stephrobert/secure-python-pipeline :
vous pouvez le cloner, inspecter les workflows et rejouer les vérifications sur la
vraie image publiée.
Anatomie du workflow et permissions
Section intitulée « Anatomie du workflow et permissions »Le workflow se déclenche à la publication d'une release et enchaîne, dans un
seul job, le build, les attestations et la signature. Le déclencheur release: [published] garantit qu'on ne signe que des versions taguées explicitement,
jamais chaque push. La partie sensible est le bloc permissions: : il part de
zéro au niveau workflow, puis n'ouvre que le strict nécessaire au niveau du job.
name: Release
on: release: types: [published]
# Aucune permission par défaut.permissions: {}
jobs: build-and-attest: name: Build, push, attester et signer runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: contents: write # attacher la provenance a la release (Signed-Releases) packages: write # push vers GHCR id-token: write # OIDC keyless (provenance + cosign) attestations: write # attestations natives GitHub env: IMAGE: ghcr.io/stephrobert/secure-python-pipelineChaque droit a une justification unique. packages: write autorise le push
vers GHCR. id-token: write délivre le jeton OIDC dont Cosign et l'action de
provenance ont besoin pour signer sans clé longue durée. attestations: write
permet de publier les attestations natives. Enfin contents: write sert
uniquement à attacher la provenance comme asset de la release, exigence du
contrôle Scorecard traitée plus bas. On ne demande rien de plus.
Build et push par digest sur GHCR
Section intitulée « Build et push par digest sur GHCR »Avant d'attester quoi que ce soit, il faut une image poussée et adressable par
digest. Le digest sha256:... est l'empreinte immuable du contenu de
l'image : c'est lui, pas le tag mutable, qui sert de sujet à toutes les
attestations. On enchaîne le durcissement du runner, l'authentification au
registre, la préparation de Buildx, le calcul des tags, puis le build.
steps: - name: Harden runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Login GHCR uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Setup Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Métadonnées (tags et labels) id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ${{ env.IMAGE }} tags: | type=semver,pattern={{version}} type=raw,value=latest - name: Build et push id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }}Deux détails portent tout le reste. Le step de connexion utilise le
GITHUB_TOKEN comme mot de passe : aucun secret longue durée ne transite. Et
le step Build et push porte un id: build, ce qui expose sa sortie
steps.build.outputs.digest. Ce digest est la valeur qu'on passera, identique,
à chaque étape d'attestation. docker/metadata-action génère au passage un tag
semver (le numéro de version) et le tag latest, pour que l'image soit
retrouvable des deux façons.
Provenance SLSA native
Section intitulée « Provenance SLSA native »La provenance répond à la question « quel workflow, sur quel commit, a produit cette image ? ». Depuis fin 2024, GitHub fournit une action native qui génère une attestation de provenance SLSA signée, sans déployer de générateur tiers ni coder un layout in-toto à la main. On la place juste après le build et on lui donne le digest de l'image poussée.
- name: Attestation de provenance SLSA (native) id: attest uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 with: subject-name: ${{ env.IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: trueL'option push-to-registry: true rattache l'attestation à l'image dans GHCR :
elle voyage avec elle et reste vérifiable par n'importe qui. Le step porte un
id: attest car il expose un bundle-path, le chemin local du bundle
d'attestation, qu'on réutilisera pour la release. Techniquement, cette action donne
SLSA niveau L2 directement, et L3 via un workflow réutilisable ; elle est
plus simple et plus sûre qu'un générateur tiers, et c'est la pratique recommandée
aujourd'hui.
SBOM CycloneDX attesté
Section intitulée « SBOM CycloneDX attesté »Un SBOM (Software Bill of Materials, l'inventaire exhaustif des composants de l'image) répond à « qu'y a-t-il dedans ? ». On le génère avec Syft au format CycloneDX JSON, puis on l'atteste en le liant au même digest. La pratique moderne est d'attacher le SBOM à l'image dans le registre, pas de le laisser traîner en fichier détaché que personne ne relie à rien.
- name: Générer le SBOM (Syft, CycloneDX) uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: image: ${{ env.IMAGE }}@${{ steps.build.outputs.digest }} format: cyclonedx-json output-file: sbom.cdx.json # Le SBOM est attesté au registre (attest-sbom) : pas besoin de # l'attacher à la release, ce qui exigerait contents: write. upload-release-assets: false - name: Attester le SBOM uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 with: subject-name: ${{ env.IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} sbom-path: sbom.cdx.json push-to-registry: trueSignature Cosign keyless
Section intitulée « Signature Cosign keyless »La signature keyless (sans clé longue durée) s'appuie sur l'identité OIDC du workflow. Cosign demande un certificat court à l'autorité Fulcio, signe le digest, puis enregistre l'opération dans le log de transparence public Rekor. Il n'y a aucune clé privée à stocker ni à faire tourner : l'identité du workflow fait foi, et Rekor rend la signature auditable par tous.
- name: Installer Cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Signer l'image (keyless Sigstore) env: IMAGE: ${{ env.IMAGE }} DIGEST: ${{ steps.build.outputs.digest }} run: cosign sign --yes "${IMAGE}@${DIGEST}"Deux points de vigilance. Le digest ne passe jamais directement dans le run:
en interpolation ${{ }} : il transite par un bloc env:, et la commande utilise
${DIGEST}. C'est la parade contre l'injection de template, appliquée
systématiquement dès qu'une donnée dynamique entre dans un run:. L'autre point est
le drapeau --yes, qui saute la confirmation interactive (impossible en CI).
La version de l'installeur, elle, cache un piège traité en fin de guide.
Attacher la provenance à la release
Section intitulée « Attacher la provenance à la release »Un dernier step échappe souvent à l'attention. Le contrôle Signed-Releases
d'OpenSSF Scorecard ne regarde pas l'attestation OCI rattachée à l'image : il
cherche un fichier *.intoto.jsonl attaché comme asset de la release. On copie
donc le bundle produit par le step attest sous ce nom, et on l'attache à la
release avec gh.
- name: Attacher la provenance à la release (Scorecard Signed-Releases) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ github.event.release.tag_name }} BUNDLE: ${{ steps.attest.outputs.bundle-path }} run: | cp "$BUNDLE" provenance.intoto.jsonl gh release upload "$TAG" provenance.intoto.jsonl --clobberC'est précisément ce step qui débloque Signed-Releases 10 dans Scorecard ; le détail du calcul (moyenne sur les cinq dernières releases, plancher par division entière) est traité dans le guide Scoring et durcissement. Ici, retenez que l'attestation OCI et l'asset de release sont deux artefacts distincts : l'un sert la vérification tierce, l'autre le scoring.
Vérifier la chaîne en tant que tiers
Section intitulée « Vérifier la chaîne en tant que tiers »Une attestation qui ne se vérifie pas ne vaut rien. Sur un dépôt public, avec
Rekor ouvert, n'importe qui peut contrôler la provenance et la signature sans
accès particulier au dépôt. Deux commandes suffisent : gh attestation verify pour
la provenance native, et cosign verify pour la signature keyless.
# Provenance native GitHub (attestation OCI rattachée à l'image)gh attestation verify oci://ghcr.io/stephrobert/secure-python-pipeline:1.0.0 \ --owner stephrobertLa sortie confirme le digest chargé et la validation des attestations trouvées :
Loaded digest sha256:… for oci://ghcr.io/stephrobert/secure-python-pipeline:1.0.0Loaded 2 attestations from GitHub API
✓ Verification succeeded!
sha256:… was attested by:REPO PREDICATE_TYPE WORKFLOWstephrobert/secure-python-pipeline https://slsa.dev/provenance/v1 .github/workflows/release.yml@refs/tags/v1.0.0La signature Cosign se vérifie ensuite. Les deux options de certificat sont
obligatoires : sans elles, cosign verify accepterait n'importe quelle
identité, ce qui viderait la vérification de son sens.
# Signature Cosign keyless : identité du workflow + émetteur OIDC obligatoirescosign verify ghcr.io/stephrobert/secure-python-pipeline:1.0.0 \ --certificate-identity-regexp "https://github.com/stephrobert/secure-python-pipeline/.github/workflows/release.yml@.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com"Une vérification réussie liste les contrôles passés et rend le payload de signature :
Verification for ghcr.io/stephrobert/secure-python-pipeline:1.0.0 --The following checks were performed on each of these signatures: - The cosign claims were validated - Existence of the claims in the transparency log was verified offline - The code-signing certificate was verified using trusted certificate authority certificatesSur l'image v1.0.0 du dépôt, ces contrôles renvoient trois attestations
rattachées au même digest : la provenance SLSA v1, la signature Cosign, et
le SBOM CycloneDX. Le détail par commande est approfondi dans le guide
Vérifier les attestations.
Automatiser la vérification
Section intitulée « Automatiser la vérification »Vérifier à la main est utile pour comprendre ; en pratique, on outille la
vérification pour qu'un mainteneur puisse la rejouer sans mémoriser les options. Le
workflow verify-slsa.yml fait exactement cela. Il se déclenche manuellement
(workflow_dispatch) avec le tag en entrée, ce qui évite la course avec le push
d'image d'une release fraîche, et n'exige que des permissions en lecture.
name: Verify SLSA
# Vérifie la provenance et la signature de l'image publiée. Déclenchement manuel# (après une release) pour éviter toute course avec le push de l'image.on: workflow_dispatch: inputs: tag: description: "Tag de l'image à vérifier (ex : 1.0.0 ou latest)" required: true default: "latest"
permissions: {}
jobs: verify: name: Vérifier provenance et signature runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: contents: read packages: read env: IMAGE: ghcr.io/stephrobert/secure-python-pipeline steps: - name: Harden runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: Installer Cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Vérifier l'attestation de provenance (native GitHub) env: IMAGE: ${{ env.IMAGE }} TAG: ${{ inputs.tag }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh attestation verify "oci://${IMAGE}:${TAG}" --owner stephrobert - name: Vérifier la signature Cosign (keyless) env: IMAGE: ${{ env.IMAGE }} TAG: ${{ inputs.tag }} run: | cosign verify "${IMAGE}:${TAG}" \ --certificate-identity-regexp "https://github.com/stephrobert/secure-python-pipeline/.github/workflows/release.yml@.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com"Ce workflow installe le même Cosign que le pipeline de release, ce qui écarte
d'emblée le piège de version décrit ci-dessous. Le tag et l'image passent par
env:, jamais en interpolation directe dans le run:, cohérent avec la règle
d'injection appliquée partout dans la série.
Le piège de la version de Cosign
Section intitulée « Le piège de la version de Cosign »Ce piège fait perdre des heures parce qu'il produit un faux négatif crédible.
L'installeur sigstore/cosign-installer@v4 pose Cosign 3.x, dont le format de
signature et de bundle diffère de celui de la 2.x. Si votre poste a encore un
Cosign 2.x installé, un cosign verify local renvoie un no signatures found
alors que l'image est parfaitement signée. Le problème n'est pas la release :
c'est l'outil local qui ne sait pas lire le nouveau format.
La parade tient en une ligne de discipline : installer Cosign par le même moyen
qu'en CI (l'installeur Sigstore, épinglé par SHA), et confirmer la version au
moindre doute. C'est aussi pourquoi verify-slsa.yml réutilise l'installeur plutôt
qu'un binaire pré-provisionné : le workflow de vérification et le workflow de
release restent alignés par construction.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »- Le workflow de release part de
permissions: {}et n'ouvre que quatre droits justifiés :packages,id-token,attestations, etcontents: writepour l'asset de release. - Le digest
sha256:...exposé par le stepbuildest le sujet unique de toutes les attestations : provenance, SBOM et signature pointent la même empreinte. - L'action native
attest-build-provenanceproduit une provenance SLSA signée (L2 direct, L3 via workflow réutilisable), sans générateur tiers. - Le SBOM CycloneDX est attesté au registre ; on coupe son attachement
redondant à la release avec
upload-release-assets: false. - La signature Cosign keyless s'ancre sur l'identité OIDC du workflow, via Fulcio et Rekor, sans clé longue durée.
- Signed-Releases de Scorecard exige un
*.intoto.jsonlattaché à la release, distinct de l'attestation OCI. - Un tiers vérifie tout avec
gh attestation verifyetcosign verify, ce dernier toujours avec--certificate-identity-regexpet--certificate-oidc-issuer. - Un
no signatures foundlocal vient souvent d'un Cosign 2.x face à une image signée en 3.x : aligner la version avant de conclure.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Dépôt public de référence : cloner le vrai dépôt, lire
release.ymlet rejouer les vérifications. - Retour au hub du lab : la série complète et le dépôt de référence.
- Attestations natives GitHub : le fonctionnement des attestations de provenance et de SBOM.
- Vérifier les attestations : le détail de
gh attestation verifyetcosign verify.