La signature Cosign keyless rattache une image de conteneur à l’identité du workflow qui l’a construite, sans clé privée à conserver : le jeton OIDC émis par GitHub Actions est échangé contre un certificat de courte durée, et la signature est inscrite dans un journal de transparence public. Attachez ensuite à l’image une attestation de provenance SLSA et un document VEX, puis vérifiez la signature contre l’identité attendue avant chaque déploiement. Signer sans vérifier au déploiement ne protège de rien.
Comment fonctionne la signature Cosign keyless ?
Cosign keyless remplace la clé privée par une identité vérifiable. Le mécanisme repose sur trois composants du projet Sigstore :
- le workflow obtient de GitHub un jeton OIDC qui décrit son identité (dépôt, fichier de workflow, référence git) ;
- Fulcio, l’autorité de certification de Sigstore, émet en échange un certificat valable quelques minutes, lié à cette identité ;
- Cosign signe l’image avec une clé éphémère associée au certificat, puis inscrit la signature dans Rekor, le journal de transparence public.
Il n’existe donc aucune clé à stocker dans les secrets du dépôt, à faire tourner ou à révoquer. En contrepartie, la sécurité repose sur la protection du workflow : quiconque peut exécuter ce workflow peut produire une signature valide.
Côté GitHub Actions, le workflow doit pouvoir demander un jeton OIDC :
permissions:
contents: read
packages: write # pousser vers le registre GHCR
id-token: write # jeton OIDC pour Cosign keyless
Pourquoi signer une empreinte plutôt qu’une étiquette ?
Parce qu’une étiquette comme v1.2.0 ou latest est mutable : n’importe qui ayant le droit d’écrire dans le registre peut la faire pointer vers un autre contenu. Une empreinte (sha256:…) désigne un contenu exact et ne peut pas être réassignée.
La règle s’applique aux deux extrémités de la chaîne :
- à la construction, on signe
image@sha256:…, l’empreinte que le build vient de produire ; - au déploiement, on résout l’étiquette en empreinte, on vérifie la signature de cette empreinte, et on déploie cette empreinte.
Pour une image multi-architecture, la signature porte sur l’empreinte du manifeste d’index, celle que le déploiement référencera, et non sur l’image d’une architecture particulière.
Le même raisonnement vaut en amont, pour les images de base : un FROM node:22-alpine suit l’étiquette, un FROM node:22-alpine@sha256:… fige le contenu. L’empreinte figée accumule les vulnérabilités publiées après coup ; un outil comme Dependabot propose alors la nouvelle empreinte par pull request, soumise aux mêmes contrôles que le reste du code.
Que contient une attestation de provenance SLSA ?
Une attestation de provenance décrit comment l’image a été produite : quel constructeur, à partir de quelle source, à quel commit, avec quels paramètres. SLSA (Supply-chain Levels for Software Artifacts) en définit le format et des niveaux d’exigence.
Avec Cosign, la provenance est un prédicat JSON attaché à l’image et signé par la même identité keyless :
cosign attest --yes --type slsaprovenance \
--predicate provenance.json \
"ghcr.io/OWNER/IMAGE@sha256:<empreinte>"
Une distinction compte. Quand une plateforme de construction isolée génère elle-même la provenance, le contenu du prédicat est garanti par cette plateforme. Quand le workflow écrit son propre prédicat, le lien cryptographique avec l’identité OIDC reste réel, mais le contenu est déclaré par la construction. Il faut le dire tel quel plutôt que revendiquer un niveau SLSA élevé.
À quoi sert une attestation OpenVEX attachée à l’image ?
Un document VEX (Vulnerability Exploitability eXchange) déclare, pour une vulnérabilité donnée, si le produit est affecté, avec une justification. OpenVEX en est un format ouvert et minimal. Exemple de statut : not_affected, justifié par vulnerable_code_not_in_execute_path.
L’attacher à l’image comme attestation signée fait voyager la qualification avec l’artefact. Un client qui scanne l’image et trouve la vulnérabilité peut vérifier que l’éditeur l’a qualifiée, et que cette qualification vient bien de la chaîne de construction officielle :
cosign attest --yes --type openvex \
--predicate security/vex/produit.openvex.json \
"ghcr.io/OWNER/IMAGE@sha256:<empreinte>"
Comment vérifier une image avant de déployer ?
En exigeant une identité précise, pas seulement « une signature valide ». Une signature keyless valide prouve que quelqu’un disposant d’une identité OIDC a signé ; il faut vérifier que c’est le bon workflow, du bon dépôt, lancé par la bonne référence :
cosign verify \
--certificate-identity-regexp '^https://github\.com/OWNER/REPO/\.github/workflows/release-images\.yml@refs/tags/v1\.2\.3$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"registre.example/IMAGE@sha256:<empreinte>"
Points de vigilance :
- ancrer l’expression (
^…$) et échapper les points, sinonv1.2.3accepte aussiv1x2x3; - arrêter l’identité à l’étiquette livrée plutôt qu’à l’arobase : une identité qui s’arrête à
release-images.yml@accepte une signature produite depuis n’importe quelle branche ; - vérifier aussi l’émetteur OIDC, pour écarter un certificat obtenu auprès d’un autre fournisseur ;
- vérifier les attestations séparément si elles conditionnent la décision :
cosign verify-attestation --type slsaprovenanceou--type openvex, avec les mêmes options d’identité.
La signature atteste l’origine, pas la qualité. Une image construite depuis un commit dont les tests sont rouges sera signée correctement si rien ne l’empêche : le contrôle « CI verte » doit exister séparément.
Comment saas-core l’intègre
saas-core signe ses images dans .github/workflows/release-images.yml, déclenché par une étiquette v* ou manuellement. Le workflow déclare packages: write et id-token: write, et aucun secret externe.
Ce que fait le workflow, dans l’ordre :
- Construction par empreinte : job
build, une construction par cible duDockerfile(api,worker,admin), poussée sans étiquette (push-by-digest) ; le jobmerge-signréunit les architectures en un manifeste unique et lit son empreinte. - Signature keyless :
cosign sign --yessurimage@<empreinte du manifeste>, avec une version de Cosign fixée parcosign-release: v2.6.1. Le testscripts/cosign-version.test.mjsvérifie que cette version est déclarée. - Provenance SLSA : un prédicat écrit par le workflow (dépôt, référence, SHA, identifiant de run) puis attaché par
cosign attest --type slsaprovenance. Le commentaire du workflow explique pourquoiactions/attest-build-provenancen’est pas utilisé (magasin d’attestations indisponible pour un dépôt privé appartenant à un utilisateur) et précise que le prédicat est déclaré par la construction elle-même. Le prédicat indique"reproducible": falseet une complétude partielle. - VEX :
security/vex/saas-core.openvex.json(format OpenVEX 0.2.0, un énoncénot_affectedsur esbuild) est attaché parcosign attest --type openvex. - Auto-vérification :
cosign verifyavec l’identité du workflow et l’émetteur GitHub. Le commentaire reconnaît une garantie faible, puisque le même acteur signe et vérifie, mais l’étape échoue si la chaîne est cassée. - Recopie par empreinte vers le registre local de la machine cible avec
crane, signatures et attestations comprises ; l’étape échoue si l’empreinte d’arrivée diffère de l’empreinte signée.
Ce qui est bloquant au déploiement : le job livrer refuse de continuer si ci ou e2e ne sont pas verts sur le commit étiqueté. Le script de livraison du dépôt d’infrastructure exécute ensuite cosign verify pour chaque image, avec une identité ancrée sur refs/tags/<étiquette>, et s’arrête sans rien écrire si une signature est absente ou invalide. Selon la fiche developpeur/docs/architecture/outils/chaine-logicielle/cosign.md, il vérifie la signature mais pas les attestations de provenance et de VEX, qui restent vérifiables par un tiers.
En amont, le Dockerfile épingle ses images de base node:22-alpine et nginx par empreinte, et .github/dependabot.yml propose chaque lundi les empreintes suivantes, sans fusion automatique.
Pour aller plus loin
L’attestation VEX ne prend son sens qu’avec un scan des dépendances qui la consulte : voir OSV-Scanner, pnpm audit et VEX. L’enchaînement complet, des tests à l’étiquette puis à la livraison, est décrit dans le pipeline CI/CD GitHub Actions d’un SaaS.
Le contenu du kit saas-core, chaîne de signature comprise, est détaillé sur la page tarifs.