Aller au contenu
Sécurité medium

Cosign : signer et vérifier vos images conteneurs

23 min de lecture

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é.

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

Section intitulée « 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 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

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.

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

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

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)
Fenêtre de terminal
# 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 :

Fenêtre de terminal
cosign version
# Exemple sortie :
# GitVersion: v3.0.4
# GitCommit: (hash du commit)
# Platform: linux/amd64
  • 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.)

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}"

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

Fenêtre de terminal
# 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}"

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

Fenêtre de terminal
# 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)

Section intitulée « 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).

#!/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

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

Fenêtre de terminal
# 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"
Fenêtre de terminal
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
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 }}

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

Fenêtre de terminal
cosign generate-key-pair
# Entrer une passphrase
# Crée : cosign.key (privée), cosign.pub (publique)
Fenêtre de terminal
cosign sign --key cosign.key ghcr.io/mon-org/mon-app@sha256:abc123
Fenêtre de terminal
cosign verify --key cosign.pub ghcr.io/mon-org/mon-app@sha256:abc123
Fenêtre de terminal
# 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://).

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/"
Fenêtre de terminal
# 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

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

Fenêtre de terminal
# 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

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

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 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

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

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}"

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

Fenêtre de terminal
# 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=".*"

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

permissions:
id-token: write # Ajouter cette ligne

Cause : pas de fournisseur OIDC disponible.

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

Fenêtre de terminal
# Mode interactif (ouvre navigateur)
COSIGN_EXPERIMENTAL=1 cosign sign --yes ghcr.io/mon-org/test@sha256:abc123
ErreurCauseSolution
no matching signaturesImage non signée ou contraintes d’identité incorrectesVérifier avec --certificate-identity-regexp=".*" pour debug
OIDC token request failedPermission id-token: write manquanteAjouter la permission au job
bundle verification failedAncienne version Cosign (< 2.6.2 / 3.0.4)Mettre à jour immédiatement (GHSA-whqx-f9j3-ch6m)
certificate identity mismatchLe workflow a changé (nom, branche)Adapter le regexp ou re-signer
Rekor entry not foundSignature avec --tlog-upload=falseUtiliser --insecure-ignore-tlog pour vérifier (non recommandé)
  1. Cosign signe cryptographiquement les images pour prouver leur authenticité
  2. Le mode keyless (recommandé) utilise OIDC + Fulcio + Rekor, sans clés à gérer
  3. Signez toujours par digest, pas par tag
  4. Mettez à jour vers v3.0.4 pour corriger GHSA-whqx-f9j3-ch6m
  5. Attestez vos SBOM avec cosign attest pour les lier à l’image
  6. Vérifiez l’identité du signataire avec --certificate-identity-regexp
  7. Déployez Kyverno ou Policy Controller pour bloquer les images non signées
  8. Cosign répond aux exigences SLSA Build L2 (provenance signée)

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.