Aller au contenu

Cosign : signer et vérifier vos images conteneurs

Mise à jour :

Vous avez construit une image conteneur, scanné ses vulnérabilités, généré un SBOM. Mais comment prouver que cette image provient bien de votre pipeline CI/CD et n’a pas été modifiée après le build ? C’est exactement ce que résout Cosign : signer cryptographiquement vos artefacts pour garantir leur authenticité et leur intégrité.

Dans le contexte SLSA, Cosign est l’outil qui permet d’atteindre Build L2 (provenance signée) : la signature cryptographique prouve que l’artefact a été construit par une plateforme de confiance et n’a pas été altéré.

Pourquoi signer vos images conteneurs ?

Le problème : registries compromis et images altérées

Sans signature, rien ne prouve qu’une image mon-app:v2.0.0 :

  • A vraiment été construite par votre CI/CD (et pas par un attaquant)
  • N’a pas été modifiée après le build (injection de code malveillant)
  • Correspond au code source versionné (et pas à une version modifiée)

Scénario d’attaque : un attaquant compromet votre registry Docker ou intercepte le push. Il remplace votre image par une version malveillante avec le même tag. Vos clusters Kubernetes déploient cette image corrompue en toute confiance.

La solution : signature cryptographique

La signature établit une chaîne de confiance vérifiable :

  1. Authenticité : l’image provient bien de l’identité déclarée (votre pipeline)
  2. Intégrité : l’image n’a pas été modifiée depuis sa signature
  3. Non-répudiation : la signature est enregistrée dans un log public immuable
Architecture Sigstore : Cosign, Fulcio, Rekor
L’écosystème Sigstore : Cosign signe, Fulcio certifie l’identité, Rekor enregistre

L’écosystème Sigstore

Cosign fait partie de Sigstore, un projet de l’OpenSSF qui fournit une infrastructure complète de signature :

Cosign

Client CLI pour signer et vérifier images, blobs, SBOM. C’est l’outil que vous utilisez directement.

Sigstore Policy Controller

Admission controller Kubernetes qui vérifie les signatures avant d’autoriser le déploiement des pods.

Pour comprendre en détail l’architecture et les concepts de signature keyless, consultez le guide Sigstore.

Keyless vs clés traditionnelles

Cosign 2.x privilégie le mode keyless (sans gestion de clés) pour la plupart des cas d’usage.

Signature keyless (recommandée)

En mode keyless, vous n’avez pas de clé privée à gérer. Cosign utilise votre identité OIDC (GitHub Actions, GitLab CI, Google Cloud) pour obtenir un certificat éphémère de Fulcio.

Avantages :

  • Pas de secrets à stocker ni à faire tourner
  • Certificat valide uniquement quelques minutes (réduction de la surface d’attaque)
  • Identité liée au pipeline CI/CD (traçabilité)
  • Signature enregistrée dans Rekor (auditabilité)
Flux de signature keyless Cosign
Signature keyless : authentification OIDC → certificat Fulcio → enregistrement Rekor

Signature avec clés (cas spécifiques)

Les clés restent utiles pour :

  • Environnements air-gapped sans accès à Sigstore public
  • Politiques de conformité exigeant des clés gérées par l’organisation
  • Intégration avec un KMS existant (AWS KMS, GCP KMS, Azure Key Vault, Vault)

Installation

Terminal window
# Télécharger la dernière version
COSIGN_VERSION=$(curl -s https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -LO "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64"
# Installer
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
# Vérifier
cosign version

Vérifiez l’installation :

Terminal window
cosign version
# Exemple sortie :
# GitVersion: v2.4.1
# GitCommit: 9a4cfe1aae777984c07ce373d97a65428bbff734
# Platform: linux/amd64

Signer une image (keyless)

Prérequis

  • Une image poussée dans un registry OCI (GHCR, Docker Hub, ECR, GCR, Harbor…)
  • Un fournisseur OIDC configuré (GitHub Actions, GitLab CI, Google Cloud, etc.)

Signature en CI/CD (GitHub Actions)

Dans un workflow GitHub Actions, la signature keyless utilise automatiquement le token OIDC du job :

name: Build and Sign
on:
push:
tags: ['v*']
# Permissions minimales au top-level
permissions:
contents: read
jobs:
build-sign:
runs-on: ubuntu-24.04
# Permissions spécifiques au job
permissions:
contents: read
packages: write # Pour push vers GHCR
id-token: write # Pour OIDC Sigstore
attestations: write # Pour GitHub Attestations
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern=v{{version}}
- name: Build and push
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true # Génère l'attestation SLSA
sbom: true # Génère le SBOM avec Syft
- name: Generate SLSA attestation
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Sign image with Cosign
run: |
IMAGE="ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}"
echo "Signing: ${IMAGE}"
cosign sign --yes "${IMAGE}"
# Vérification immédiate de la signature
echo "Verifying signature..."
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
"${IMAGE}"

Signature interactive (développement)

Pour tester localement, Cosign ouvre un navigateur pour l’authentification OIDC :

Terminal window
# Pousser une image de test
docker build -t ghcr.io/mon-org/test:dev .
docker push ghcr.io/mon-org/test:dev
# Récupérer le digest
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/mon-org/test:dev | cut -d'@' -f2)
# Signer (ouvre le navigateur pour authentification)
cosign sign --yes "ghcr.io/mon-org/test@${DIGEST}"

Vérifier une signature

La vérification s’assure que l’image a été signée par une identité de confiance.

Vérifier une image signée en keyless

Terminal window
# Vérifier avec contraintes d'identité
cosign verify ghcr.io/mon-org/mon-app@sha256:abc123... \
--certificate-identity-regexp="^https://github.com/mon-org/mon-app" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com"

Paramètres clés :

  • --certificate-identity-regexp : regex sur l’identité du signataire (URL du workflow)
  • --certificate-oidc-issuer : l’émetteur OIDC attendu (GitHub Actions, GitLab, etc.)

Sortie en cas de succès :

[{
"critical": {
"identity": { "docker-reference": "ghcr.io/mon-org/mon-app" },
"image": { "docker-manifest-digest": "sha256:abc123..." },
"type": "cosign container image signature"
},
"optional": {
"Issuer": "https://token.actions.githubusercontent.com",
"Subject": "https://github.com/mon-org/mon-app/.github/workflows/build.yml@refs/heads/main",
...
}
}]

Cas particulier : GitHub Container Registry (GHCR)

GHCR ne supporte pas encore complètement OCI 1.1 referrers. Les signatures Cosign ne sont pas attachées directement à l’image dans le registry, elles existent uniquement dans Rekor (le log de transparence).

Vérifier dans un script CI

#!/bin/bash
set -e
IMAGE="ghcr.io/mon-org/mon-app@sha256:${DIGEST}"
if cosign verify "${IMAGE}" \
--certificate-identity-regexp="^https://github.com/mon-org/" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
> /dev/null 2>&1; then
echo "✅ Signature valide"
else
echo "❌ Signature invalide ou absente"
exit 1
fi

Attacher un SBOM signé

Un SBOM non signé peut être falsifié. Cosign permet d’attester un SBOM : le lier cryptographiquement à l’image avec une signature.

Générer et attester un SBOM

Terminal window
# Générer le SBOM avec Syft
syft ghcr.io/mon-org/mon-app@sha256:abc123 -o cyclonedx-json > sbom.cdx.json
# Attester le SBOM (keyless)
cosign attest --yes \
--predicate sbom.cdx.json \
--type cyclonedx \
"ghcr.io/mon-org/mon-app@sha256:abc123"

Vérifier une attestation SBOM

Terminal window
cosign verify-attestation \
--type cyclonedx \
--certificate-identity-regexp="^https://github.com/mon-org/" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/mon-org/mon-app@sha256:abc123

Workflow complet : build, sign, attest

jobs:
build-and-sign:
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
id-token: write
attestations: write
outputs:
digest: ${{ steps.build.outputs.digest }}
image: ghcr.io/${{ github.repository }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
provenance: true
sbom: true
- name: Generate SLSA attestation
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Sign image
env:
IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "${IMAGE}"
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
"${IMAGE}"
sbom-detailed:
runs-on: ubuntu-24.04
needs: build-and-sign
permissions:
contents: read
packages: read
id-token: write
steps:
- name: Install Syft
uses: anchore/sbom-action/download-syft@a930d0ac434e3182448fe678398ba5713717112a # v0.21.0
- name: Generate SBOM (SPDX)
run: |
syft ${{ needs.build-and-sign.outputs.image }}@${{ needs.build-and-sign.outputs.digest }} \
-o spdx-json=sbom-spdx.json
- name: Generate SBOM (CycloneDX)
run: |
syft ${{ needs.build-and-sign.outputs.image }}@${{ needs.build-and-sign.outputs.digest }} \
-o cyclonedx-json=sbom-cyclonedx.json
- name: Upload SBOM artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: sbom
path: |
sbom-spdx.json
sbom-cyclonedx.json
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Attach SBOM to image
run: |
cosign attach sbom \
--sbom sbom-spdx.json \
${{ needs.build-and-sign.outputs.image }}@${{ needs.build-and-sign.outputs.digest }}

Signature avec clés

Pour les environnements nécessitant des clés gérées.

Générer une paire de clés

Terminal window
cosign generate-key-pair
# Entrer une passphrase
# Crée : cosign.key (privée), cosign.pub (publique)

Signer avec une clé

Terminal window
cosign sign --key cosign.key ghcr.io/mon-org/mon-app@sha256:abc123

Vérifier avec la clé publique

Terminal window
cosign verify --key cosign.pub ghcr.io/mon-org/mon-app@sha256:abc123

Utiliser un KMS

Terminal window
# Générer la clé dans AWS KMS
cosign generate-key-pair --kms awskms://arn:aws:kms:eu-west-1:123456789:key/abcd-1234
# Signer
cosign sign --key awskms://arn:aws:kms:eu-west-1:123456789:key/abcd-1234 \
ghcr.io/mon-org/mon-app@sha256:abc123
# Vérifier
cosign verify --key awskms://arn:aws:kms:eu-west-1:123456789:key/abcd-1234 \
ghcr.io/mon-org/mon-app@sha256:abc123

KMS supportés : AWS KMS (awskms://), GCP KMS (gcpkms://), Azure Key Vault (azurekms://), HashiCorp Vault (hashivault://), Kubernetes Secrets (k8s://).

Vérifier les signatures dans Kubernetes

Avec Kyverno

Kyverno peut bloquer le déploiement d’images non signées :

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/mon-org/*"
attestors:
- entries:
- keyless:
issuer: "https://token.actions.githubusercontent.com"
subjectRegExp: "^https://github.com/mon-org/"

Avec Sigstore Policy Controller

Terminal window
# Installer le policy controller
helm repo add sigstore https://sigstore.github.io/helm-charts
helm install policy-controller sigstore/policy-controller \
-n sigstore-system --create-namespace

Vérifier les attestations SLSA

GitHub fournit une API pour vérifier les attestations de provenance :

Vérification avec GitHub CLI

Terminal window
# Vérifier une image avec gh attestation
gh attestation verify oci://ghcr.io/owner/repo:tag --owner owner

Sortie en cas de succès :

Loaded digest sha256:abc123... for oci://ghcr.io/owner/repo:tag
Loaded 2 attestations from GitHub API
✓ Verification succeeded!
sha256:abc123... was attested by:
REPOSITORY PREDICATE_TYPE WORKFLOW
owner/repo https://slsa.dev/provenance/v1 .github/workflows/release.yml@refs/tags/v1.0.0

Workflow de vérification automatique

Créez un workflow qui vérifie périodiquement vos attestations :

name: SLSA Verification
on:
schedule:
- cron: '0 2 * * *' # Toutes les nuits à 2h
workflow_dispatch:
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-24.04
steps:
- name: Get latest tag
id: latest
run: |
LATEST_TAG=$(gh api repos/${{ github.repository }}/tags --jq '.[0].name')
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get image digest
id: digest
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ steps.latest.outputs.tag }}"
DIGEST=$(docker pull "${IMAGE}" 2>&1 | grep "Digest:" | cut -d' ' -f2)
echo "digest=$DIGEST" >> $GITHUB_OUTPUT
- name: Verify SLSA attestation
run: |
gh attestation verify \
oci://ghcr.io/${{ github.repository }}@${{ steps.digest.outputs.digest }} \
--owner ${{ github.repository_owner }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Report status
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
echo "✅ SLSA verification passed for ${{ steps.latest.outputs.tag }}"
else
echo "❌ SLSA verification failed for ${{ steps.latest.outputs.tag }}"
exit 1
fi

Sécurité

Bonnes pratiques

Keyless par défaut

Privilégiez la signature keyless pour éviter la gestion de secrets. Les clés ne sont utiles que pour les environnements air-gapped.

Signer par digest

Toujours signer image@sha256:..., jamais image:tag. Un tag peut être réassigné, pas un digest.

Vérifier l'identité

Utilisez --certificate-identity-regexp pour vérifier que le signataire est bien votre pipeline CI/CD.

Automatiser la vérification

Déployez Kyverno ou Policy Controller pour bloquer les images non signées dans Kubernetes.

Permissions GitHub Actions

# Permissions minimales au niveau workflow
permissions:
contents: read
jobs:
build:
# Permissions spécifiques au job
permissions:
contents: read # Lire le code
packages: write # Pousser images et signatures
id-token: write # Obtenir token OIDC pour Sigstore
attestations: write # Générer attestations GitHub

Actions GitHub pinnées par SHA

Toujours utiliser des SHA de commit pour éviter les attaques supply chain :

steps:
# ❌ Mauvais : version flottante
- uses: actions/checkout@v4
# ✅ Bon : SHA pinné avec commentaire de version
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

Vérification post-signature

Toujours vérifier la signature immédiatement après l’avoir créée :

- name: Sign and verify
run: |
IMAGE="ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}"
# Signer
cosign sign --yes "${IMAGE}"
# Vérifier immédiatement
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
"${IMAGE}"

Dépannage

”no matching signatures”

Cause : l’image n’est pas signée ou les contraintes d’identité ne correspondent pas.

Terminal window
# Lister les signatures existantes
cosign triangulate ghcr.io/mon-org/mon-app@sha256:abc123
# Vérifier sans contraintes d'identité (debug)
cosign verify ghcr.io/mon-org/mon-app@sha256:abc123 \
--certificate-identity-regexp=".*" \
--certificate-oidc-issuer-regexp=".*"

”OIDC token request failed”

Cause : permissions id-token: write manquantes dans le workflow.

permissions:
id-token: write # Ajouter cette ligne

Signature échoue en local

Cause : pas de fournisseur OIDC disponible.

Solution : utilisez l’authentification interactive ou des clés locales pour les tests.

Terminal window
# Mode interactif (ouvre navigateur)
COSIGN_EXPERIMENTAL=1 cosign sign --yes ghcr.io/mon-org/test@sha256:abc123

À retenir

  • Cosign signe cryptographiquement les images pour prouver leur authenticité
  • Le mode keyless (recommandé) utilise OIDC + Fulcio + Rekor, sans clés à gérer
  • Signez toujours par digest, pas par tag
  • Attestez vos SBOM avec cosign attest pour les lier à l’image
  • Vérifiez l’identité du signataire avec --certificate-identity-regexp
  • Déployez Kyverno ou Policy Controller pour bloquer les images non signées
  • Cosign répond aux exigences SLSA Build L2 (provenance signée)

Liens utiles