Signatures d’images : signer avec cosign, appliquer au pull
fremforge vérifie la signature cosign de chaque image de conteneur que votre organisation tire depuis frem.sh. Vous décidez à quelles signatures vous faites confiance, et si un pull sans signature est enregistré (shadow) ou refusé (enforce).
La vérification porte sur le digest exact de l’image servie, au moment du pull ; elle s’applique donc de la même façon à docker pull, à Kubernetes, aux tâches de CI et à tout autre client OCI.
Signer une image dans fremforge Actions
fremforge exploite son propre Sigstore : une autorité de certification (Fulcio) sur sign.frem.sh et une autorité d’horodatage RFC 3161 sur tsa.frem.sh. Il n’exploite aucun journal de transparence (pas de Rekor) ni aucun journal de transparence des certificats : c’est l’horodatage de tsa.frem.sh qui prouve le moment où l’image a été signée.
La racine de confiance
Le certificat racine de sign.frem.sh et la chaîne de certificats de tsa.frem.sh sont publiés sur https://www.frem.sh/.well-known/slsa-trust-root.json, sous keyless.fulcio_root_ca_pem et keyless.tsa_cert_chain_pem. Récupérez-les une fois et committez-les dans le dépôt qui signe ou vérifie : un runner hébergé atteint sign.frem.sh et tsa.frem.sh, mais pas www.frem.sh.
curl -sSf https://www.frem.sh/.well-known/slsa-trust-root.json -o slsa-trust-root.json
jq -r .keyless.fulcio_root_ca_pem slsa-trust-root.json > fulcio-root.pem
jq -r .keyless.tsa_cert_chain_pem slsa-trust-root.json > tsa-chain.pemLe sujet de la racine est CN=fremforge sigstore Root CA, O=fremverk, C=DE.
Une tâche signe avec sa propre identité à courte durée de vie : il n’y a donc aucune clé de signature à stocker ni à renouveler. Demandez au point de terminaison OIDC du runner un jeton avec l’audience sigstore. Le certificat nomme alors votre workflow :
https://frem.sh/<org>/<repo>/.forgejo/workflows/<file>@<ref>avec l’émetteur https://frem.sh/_app/runner/oidc. Le workflow a besoin de enable-openid-connect: true au premier niveau (Forgejo ne le déduit pas de permissions) et la tâche de id-token: write :
enable-openid-connect: true
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Sign the image
env:
IMAGE: frem.sh/acme/api@${{ env.DIGEST }}
TOKEN: ${{ secrets.FORGEJO_TOKEN }}
run: |
set -euo pipefail
ID_TOKEN=$(curl -sSf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sigstore" | jq -r .value)
# fulcio-root.pem and tsa-chain.pem: committed from the published
# trust root (see "The trust root" above).
cosign trusted-root create \
--fulcio "url=https://sign.frem.sh,certificate-chain=fulcio-root.pem" \
--tsa "url=https://tsa.frem.sh/api/v1/timestamp,certificate-chain=tsa-chain.pem" \
--out trusted-root.json
cosign signing-config create \
--fulcio="url=https://sign.frem.sh,api-version=1,start-time=2026-01-01T00:00:00Z,operator=frem.sh" \
--tsa="url=https://tsa.frem.sh/api/v1/timestamp,api-version=1,start-time=2026-01-01T00:00:00Z,operator=frem.sh" \
--tsa-config="EXACT:1" --out signing-config.json
echo "$TOKEN" | cosign login frem.sh -u ci --password-stdin
cosign sign --yes --signing-config signing-config.json \
--trusted-root trusted-root.json --identity-token "$ID_TOKEN" "$IMAGE"cosign est installé dans l’image du runner hébergé. Signez le digest (image@sha256:…), pas un tag : la signature est liée au digest.
Vérifier une signature
Construisez trusted-root.json avec la même commande cosign trusted-root create, puis nommez le workflow exact qui doit avoir signé :
cosign verify --trusted-root trusted-root.json \
--use-signed-timestamps --insecure-ignore-tlog --insecure-ignore-sct \
--certificate-oidc-issuer https://frem.sh/_app/runner/oidc \
--certificate-identity https://frem.sh/acme/api/.forgejo/workflows/release.yaml@refs/heads/main \
frem.sh/acme/api@sha256:…--certificate-oidc-issuervaut toujourshttps://frem.sh/_app/runner/oidcpour une tâche fremforge Actions.--certificate-identityvauthttps://frem.sh/<org>/<repo>/.forgejo/workflows/<file>@<ref>, où<ref>est la ref complète sur laquelle la tâche s’est exécutée (refs/heads/main,refs/tags/v1.2.0). Pour accepter tous les tags, utilisez--certificate-identity-regexp '^https://frem\.sh/acme/api/\.forgejo/workflows/release\.yaml@refs/tags/v.+$'.--use-signed-timestampsprend l’heure de signature dans l’horodatage detsa.frem.sh, et--insecure-ignore-tlogignore la vérification du journal de transparence, puisqu’il n’y a pas de Rekor.--insecure-ignore-sctest obligatoire, car il n’existe pas de journal de transparence des certificats : les certificats ne portent donc pas d’horodatage de certificat signé (SCT). Ce que cela coûte : aucun journal public où un certificat émis à tort apparaîtrait. Le certificat doit toujours remonter à la racine fremforge ci-dessus, porter exactement l’émetteur et l’identité que vous nommez, et être valide au moment oùtsa.frem.sha signé.
D’autres façons de signer fonctionnent aussi :
- Une paire de clés que vous gérez :
cosign sign --key cosign.key. Ajoutez la clé publique (cosign.pub) à la politique de confiance. - GitHub Actions sans clé (Sigstore public) : activez Faire aussi confiance au Sigstore public. La vérification se fait hors ligne avec une racine de confiance épinglée ; rien n’est envoyé à sigstore.dev lors d’un pull.
Les deux formats cosign sont lus : le format bundle par défaut de cosign v3 (stocké sous le tag sha256-<digest>, car le registre n’a pas d’API referrers) et l’ancien format .sig.
La politique de confiance
Définissez-la dans Sécurité › Politiques › Politiques d’images › Signatures des images au pull, avec l’API, ou avec Terraform ou OpenTofu.
| Paramètre | Signification |
|---|---|
enabled | Vérifier les signatures lorsqu’une image est tirée. |
mode | shadow (par défaut) : servir l’image et enregistrer un « aurait bloqué ». enforce : refuser le pull avec 403. |
signers | Les signataires sans clé de confiance, en une seule liste. Chacun est soit un issuer seul, qui fait confiance à tout sujet que cet émetteur certifie, soit un issuer et un subject_regex auquel le sujet complet du certificat doit correspondre. |
keys | Les clés publiques de confiance : ECDSA P-256 ou P-384, Ed25519, ou RSA d’au moins 2048 bits. |
scope | Les images concernées : des noms d’images de votre organisation, éventuellement avec un tag (api, team/*, api:prod-*). Vide signifie toutes les images. |
trust_public_sigstore | Faire aussi confiance aux signatures produites avec le Sigstore public. |
Un pull passe lorsqu’au moins une signature du digest servi se vérifie et que son signataire est de confiance : l’une de vos clés, ou un certificat qui correspond à un signataire. Si signers et keys sont tous deux vides, seules les tâches fremforge Actions de votre propre organisation sont de confiance : un certificat issu du Sigstore de fremforge, avec l’émetteur https://frem.sh/_app/runner/oidc et un sujet qui commence par https://frem.sh/<your org>/. Nommez les workflows visés dès que possible.
La portée par tag couvre aussi les pulls par digest. Un digest ne nomme aucun tag ; une entrée comme api:prod-* s’applique donc aussi à chaque pull api@sha256:…. Sinon, un pull par digest permettrait de la contourner.
Dans la console, Signataires de confiance (sans clé) est un seul champ texte : un signataire par ligne, l’URL de l’émetteur, puis éventuellement une espace et la regex de sujet. Un émetteur seul fait confiance à tout ce qu’il certifie ; pour un émetteur public comme GitHub Actions ou fremforge Actions, ajoutez donc toujours un sujet. Une liste que la page refuse vous est renvoyée avec la raison et ce que vous avez saisi, et rien n’est enregistré.
Une regex de sujet ne doit pas répéter un groupe qui se répète lui-même (par exemple (a+)+), et ne peut pas utiliser de références arrière ni d’assertions avant ou arrière : de tels motifs peuvent prendre un temps exponentiel, et ils sont refusés à l’enregistrement.
Ce que reçoit un pull
| Le digest… | shadow | enforce |
|---|---|---|
| a une signature d’un signataire de confiance | servi | servi |
| n’est pas signé | servi, registry.pull.would_block | 403 DENIED, registry.pull.blocked |
| est signé, mais pas par un signataire de confiance | servi, aurait bloqué | 403, bloqué |
| a une signature qui ne se vérifie pas, ou qui nomme une autre image | servi, aurait bloqué | 403, bloqué |
| ne peut pas être vérifié (le registre n’a pas répondu) | servi, journalisé | 503 avec Retry-After |
Le 403 indique de quel cas il s’agit, nomme le signataire s’il y en a un, et renvoie vers la page des paramètres.
Deux sortes de manifestes ne sont pas vérifiées : les signatures, attestations et SBOM rattachés aux images (afin que cosign verify lui-même continue de fonctionner), et les autres artefacts OCI comme les charts Helm. Cela se décide d’après le manifeste lui-même, et non d’après son tag.
Images multiplateformes. cosign signe normalement l’index d’image. Lorsqu’un index passe, ses manifestes de plateforme passent aussi ; le pull suivant de linux/amd64 par digest est donc servi.
Un résultat vérifié est réutilisé pendant une heure, et un échec pendant une minute : une signature que vous poussez après un refus compte donc en moins d’une minute. Une modification de la politique de confiance prend effet immédiatement.
Déploiement progressif
- Activez la vérification en
shadowet laissez-la tourner quelques jours. - Consultez les « aurait bloqué » dans le journal d’audit (
registry.pull.would_blockavecgate: signature) ou dans Vérifications de signature récentes sur la page des paramètres. - Signez ce qui manque, restreignez
scopesi nécessaire, puis passez enenforce.
API
GET et PUT /orgs/{org}/image-signature-policy sur https://frem.sh/_app/api/v1, avec policy:read et policy:write. PUT remplace toute la politique : un champ omis reçoit sa valeur par défaut.
Jusqu’au 2026-10-16, les signataires de confiance tenaient en deux champs, issuers (un émetteur avec n’importe quel sujet) et identities (un émetteur avec une regex de sujet). Les deux fonctionnent toujours et GET les renvoie encore, à côté de signers. Un PUT peut envoyer l’une ou l’autre forme, ou les trois lorsqu’elles concordent ; une lecture-modification-écriture des anciens champs continue donc de fonctionner. Des champs qui ne concordent pas sont refusés avec 400 invalid-value.
curl -sS -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
"https://frem.sh/_app/api/v1/orgs/acme/image-signature-policy" \
-d '{"enabled": true, "mode": "shadow",
"signers": [{"issuer": "https://frem.sh/_app/runner/oidc",
"subject_regex": "https://frem\\.sh/acme/[^/]+/\\.forgejo/workflows/release\\.yaml@refs/tags/v.+"}],
"scope": ["api", "worker:prod-*"]}'Une politique invalide est refusée avec 400 invalid-value, qui nomme le champ.
Audit et SIEM
- Chaque modification :
security_policy.image_signature.updated, avec ce qui a changé. - Chaque décision :
registry.pull.blockedouregistry.pull.would_blockavecgate: signatureetreason(unsigned,untrustedouinvalid), au plus une fois par heure et par digest d’image, avec le compte de l’heure précédente. Ces événements atteignent votre flux SIEM comme tout autre événement d’audit, et un blocage déclenche aussi le webhook d’alertesecurity.pull.blocked.
Résultats de signature dans les pull requests
Sur chaque pull request, fremforge vérifie les signatures des images de conteneurs que votre dépôt épingle par digest, c’est-à-dire une ligne image: se terminant par @sha256:…. La vérification parcourt les fichiers YAML, les templates Helm, les Dockerfiles et les Containerfiles, et couvre jusqu’à 50 images par analyse. Le résultat est le statut fremforge/cosign-verify sur la pull request et la colonne Cosign de la page Images de conteneurs.
Chaque image obtient l’un de ces résultats :
- Signée : une signature s’est vérifiée, et le signataire est affiché. Deux sortes de signataires sont de confiance :
- les propres workflows fremforge Actions de votre organisation, signés via le Sigstore de fremforge et vérifiés sur l’horodatage de confiance ;
- un signataire du Sigstore public, vérifié par rapport à son journal de transparence.
- Non signée : le registre a répondu et ne contient aucune signature d’un signataire de confiance. Une signature produite par le workflow d’une autre organisation compte comme non signée.
- Pas d’horodatage de confiance : le propre workflow de votre organisation a signé l’image via le Sigstore de fremforge, mais sans l’horodatage de confiance. Une telle signature ne peut plus être vérifiée une fois son certificat à courte durée de vie expiré ; elle compte donc comme non signée. Signez de nouveau avec la configuration de signature de Signer une image dans fremforge Actions.
- Vérification impossible : la vérification n’a pas pu lire la signature. La page Images de conteneurs en indique la raison, par exemple que le registre n’est pas servi par votre proxy de dépendances, que l’image se trouve dans le registre d’une autre organisation, ou que le registre n’a pas répondu. Un résultat vérification impossible n’est jamais signalé comme non signée.
La vérification lit les images via le proxy de dépendances de votre organisation, depuis le registre frem.sh de votre organisation, et depuis un registre privé pour lequel votre organisation détient un identifiant. Elle vérifie hors ligne ; elle ne contacte donc jamais elle-même le registre de l’image ni Sigstore. Les deux formats cosign sont lus.
Images dans un registre privé
Une image dans un registre extérieur à fremforge, par exemple registry.gitlab.com/acme/tool@sha256:… ou un Harbor privé, est vérifiée lorsque votre organisation a enregistré un identifiant en lecture seule pour l’hôte de ce registre :
- Activez la fonctionnalité et ajoutez un identifiant sous Sécurité › Accès › Identifiants de registre (ou via l’API des identifiants de registre). Il faut être propriétaire de l’organisation, et la fonctionnalité reste désactivée tant qu’un propriétaire ne l’a pas activée.
- Un identifiant par hôte,
kind: oci, avec une date d’expiration obligatoire à 366 jours au plus. Docker Hub, ghcr.io, quay.io et les registres Red Hat ne figurent pas dans cette liste : les images qui en proviennent utilisent plutôt les identifiants amont de votre organisation dans le proxy de dépendances. - L’identifiant reste au sein de fremforge. Le proxy de dépendances l’utilise pour lire la signature de l’image, et uniquement pour une analyse de votre organisation. L’analyseur ne le détient jamais, il n’est jamais envoyé à un autre hôte, et rien de ce qui est lu avec lui n’est mis en cache.
Lorsque l’identifiant ne peut pas être utilisé, l’image est en Vérification impossible, avec l’une de ces raisons :
- expiré : l’identifiant a dépassé sa date d’expiration. Un propriétaire modifie la date ou le remplace sous Identifiants de registre ;
- refusé : le registre a répondu 401 ou 403. Un propriétaire vérifie l’identifiant ou le remplace sous Identifiants de registre ;
- limité en débit : la vérification a atteint la limite de lecture de l’identifiant. fremforge vérifie de nouveau plus tard ;
- inutilisable : fremforge n’a pas pu utiliser l’identifiant enregistré. L’erreur vient de fremforge, qui vérifie de nouveau plus tard.
Un registre pour lequel votre organisation ne détient aucun identifiant reste en Vérification impossible, car le registre n’est pas servi par votre proxy de dépendances.
Le statut est informatif : il passe toujours et ne bloque jamais une fusion. La vérification au pull décrite plus haut est une vérification distincte.
Les images que fremforge sert depuis son propre registre de plateforme affichent « signed by fremforge » lorsque le build de version de fremforge les a signées. Pour ces images, ce build est le seul signataire de confiance, et il n’est jamais de confiance pour vos propres images.
Voir aussi
- Analyse des images et Paquets malveillants : les autres vérifications au pull.
- SBOM.