Aller au contenu principal

Releases immuables et attestation de release

Une release publiée est une promesse : ces fichiers, construits à partir de ce commit, sous ce tag. Sans immuabilité, cette promesse peut être rompue discrètement après coup : un tag redirigé vers un autre commit, un artefact remplacé par un autre binaire, une release supprimée puis recréée sous le même nom. Les releases immuables rendent chacune de ces opérations impossible, et une attestation de release permet à quiconque de vérifier, sur sa propre machine, que ce qu’il a téléchargé correspond à ce qui a été publié.

Ce qui est figé

Lorsque les releases immuables sont activées, une release est figée dès sa publication. À partir de ce moment :

ModificationAutorisée ?
Modifier le titre, les notes, l’indicateur de pré-releaseOui
Changer le nom du tag ou le commit cibleNon
Déplacer le tag avec git push --force, ou le supprimer avec git push --deleteNon
Repasser la release en brouillonNon
Ajouter, renommer, remplacer ou supprimer un artefactNon
Supprimer la releaseOui, mais son nom de tag ne pourra plus jamais être réutilisé dans ce dépôt

Chaque surface applique la même règle : l’interface web, l’API REST (HTTP 422 avec un message commençant par immutable release:) et git lui-même (le push est refusé par le hook pre-receive avec un message qui nomme le tag).

Le gel ne vaut que pour l’avenir. Activer le paramètre ne fige rien de ce qui a déjà été publié, et le désactiver plus tard ne défige rien. Si une release figée doit vraiment changer, contactez le support : le support peut lever le gel de cette seule release, avec une raison écrite qui apparaît dans votre journal d’audit.

Publiez les artefacts avant de publier la release

Une release immuable publiée n’accepte plus de nouveaux artefacts, l’ordre compte donc. Créez la release en tant que brouillon, téléversez les artefacts, puis publiez-la :

# 1. draft
curl -sS -X POST -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
  -d '{"tag_name":"v1.2.3","name":"v1.2.3","draft":true}' \
  https://frem.sh/api/v1/repos/acme/web/releases
# 2. assets (repeat per file)
curl -sS -X POST -H "Authorization: token $TOKEN" -F attachment=@dist/web-linux-amd64.tar.gz \
  "https://frem.sh/api/v1/repos/acme/web/releases/$RELEASE_ID/assets?name=web-linux-amd64.tar.gz"
# 3. publish — this is the moment it freezes
curl -sS -X PATCH -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
  -d '{"draft":false}' https://frem.sh/api/v1/repos/acme/web/releases/$RELEASE_ID

Les outils de release qui créent une release publiée puis téléversent ensuite échoueront au téléversement avec 422. Les artefacts qui ne sont que des liens externes ne peuvent pas faire partie d’une release immuable, car il n’existe aucun octet stocké dont calculer le condensé ; leur publication est refusée.

Chaque artefact porte désormais son condensé dans l’API, comme chez GitHub : "digest": "sha256:<hex>", et une release immuable indique "immutable": true.

Tags de conteneur

La même politique peut figer les tags d’images de conteneur dans le registre :

ModeFigé
off (par défaut)rien : tout tag peut être écrasé
semverles tags de version : v1.2.3, 1.2.3, 1.2.3-rc.1 ; latest, 1, 1.2, main, sha-… restent déplaçables
alltous les tags sauf les tags déplaçables que vous listez (latest, edge-*, …)

Un push d’un tag figé qui le ferait pointer vers un condensé différent est refusé avec l’erreur de registre DENIED (HTTP 403). Pousser à nouveau la même image est autorisé, de sorte qu’un job CI relancé ne casse pas. La suppression d’un tag figé est refusée, et les règles de nettoyage des paquets n’en sélectionnent jamais ; les tags figés comptent toujours dans votre quota de stockage. Les tags de signature et d’attestation qu’écrit cosign (sha256-<digest>.sig, .att, .sbom) sont toujours déplaçables. Les tags de conteneur sont évalués selon la politique en vigueur : désactiver le mode est donc la façon de déplacer ou supprimer délibérément l’un d’eux.

Paquets

Les versions de paquets publiées sont immuables sur fremforge : npm, Maven, PyPI, NuGet, Cargo, Go, Helm, Conan, Composer, RubyGems, Pub, Swift, Conda, CRAN, Debian, RPM, Alpine, Arch, Chef, Vagrant et paquets génériques. C’est activé pour chaque organisation par défaut. Les conteneurs suivent plutôt le mode des tags de conteneur ci-dessus.

ModificationAutorisée ?
Téléverser une nouvelle versionOui
Ajouter un nouveau fichier à une version existante (une autre wheel pour une plateforme, un jar Maven avec classifier)Oui
Téléverser à nouveau un fichier avec les mêmes octets (un job CI relancé)Oui, sans effet
Remplacer un fichier d’une version publiée par des octets différentsNon
Supprimer une version, ou l’un de ses fichiersOui
Publier à nouveau une version supprimée, avec n’importe quels octetsNon : le nom de version reste retiré
Téléverser à nouveau un fichier supprimé avec des octets différentsNon

Un téléversement refusé renvoie HTTP 409 avec un message commençant par immutable package:, et l’en-tête de réponse X-Fremforge-Immutable indique pourquoi : overwrite, reupload ou retired. Chaque refus est écrit dans votre journal d’audit sous package.immutable.refused, avec le paquet, la version, le fichier, les deux condensés SHA-256 et l’auteur de la tentative. Il apparaît aussi dans le journal d’accès au registre comme un téléversement bloqué.

La plupart des registres refusent déjà de publier deux fois une version. Ce qui change ici :

  • Helm et Conan permettaient d’écraser une version sur place.
  • Dans tous les registres, une version pouvait être supprimée puis republiée avec un contenu différent. Ces deux cas sont désormais refusés.

Toujours modifiables :

  • Les versions Maven -SNAPSHOT et les fichiers maven-metadata.xml, réécrits par conception.
  • Les fichiers d’index de dépôt que maintiennent les registres Debian, RPM, Alpine et Arch.

Pour garder d’autres versions modifiables, par exemple un paquet générique utilisé comme cache de CI, listez-les comme exemptions sous la forme [<type>:][<name>@]<version>, où * et ? servent de jokers :

EntréeGarde modifiable
generic:ci-cache@*toutes les versions du paquet générique ci-cache
npm:@acme/ui@0.0.0-dev*les builds de développement d’un paquet npm
*-nightlyles versions *-nightly de tous les paquets

Une organisation peut aussi désactiver entièrement l’immuabilité des paquets, ou revenir à la valeur par défaut de la plateforme (inherit). Le paramètre est évalué au moment d’une écriture, et les noms des versions supprimées pendant qu’il était désactivé restent mémorisés. L’activer protège donc aussi les versions supprimées auparavant.

Activation

Propriétaires de l’organisation : Sécurité › Politiques › Releases immuables dans la console (/<org>/_admin/security/policies/immutable-releases), ou l’API avec un jeton portant policy:write (la lecture des paramètres nécessite findings:read) :

curl -sS -X PUT -H "Authorization: Bearer $FFP_TOKEN" -H 'Content-Type: application/json' \
  -d '{"releases":true,"allow_repo_opt_out":false,"oci_mode":"semver","oci_mutable_tags":["latest"]}' \
  https://frem.sh/_app/api/v1/orgs/acme/code-security/immutability

L’immuabilité des paquets utilise le même endpoint. packages vaut inherit (suivre la valeur par défaut de la plateforme, qui est activée), on ou off, et package_mutable_versions liste les exemptions. La réponse indique aussi packages_effective :

curl -sS -X PUT -H "Authorization: Bearer $FFP_TOKEN" -H 'Content-Type: application/json' \
  -d '{"packages":"inherit","package_mutable_versions":["generic:ci-cache@*"]}' \
  https://frem.sh/_app/api/v1/orgs/acme/code-security/immutability

Un dépôt peut surcharger l’organisation : on y fige toujours les releases, même lorsque la valeur par défaut de l’organisation est désactivée ; off est un désengagement, respecté uniquement si l’organisation l’autorise ; inherit suit l’organisation.

curl -sS -X PUT -H "Authorization: Bearer $FFP_TOKEN" -H 'Content-Type: application/json' \
  -d '{"releases":"on"}' https://frem.sh/_app/api/v1/orgs/acme/code-security/immutability/repos/web

Les miroirs (pull mirrors) ne sont jamais figés : leurs tags appartiennent au dépôt qu’ils reflètent.

L’attestation de release

Pour chaque release immuable, fremforge signe une déclaration in-toto de type https://in-toto.io/attestation/release/v0.1. Ses sujets sont :

  • chaque artefact, par nom et SHA-256 (calculé pendant le téléversement du fichier) ;
  • la source, git+https://frem.sh/<org>/<repo>@refs/tags/<tag>, par gitCommit.

La déclaration est tirée d’un manifeste que Forgejo écrit dans la même transaction que celle qui publie la release, et revérifiée par rapport à la release en ligne avant d’être signée. Elle est signée avec les mêmes clés que toutes les autres attestations fremforge (une signature DSSE Ed25519, plus un bundle Sigstore sans clé issu du Fulcio et de l’autorité d’horodatage propres à fremforge) et inscrite dans le journal de transparence. L’attestation est servie par l’API et la CLI ; elle n’est jamais jointe à la release elle-même.

La signature ne retarde jamais une publication. Une release est d’abord publiée puis signée peu après, si bien que pendant un court moment son attestation est en attente. En attente ne signifie pas réussie : la vérification indique PENDING et se termine avec son propre code.

Vérifier une release

fremforge release verify acme/web v1.2.3 ./web-linux-amd64.tar.gz ./checksums.txt

La commande récupère l’attestation, vérifie la signature par rapport à la racine de confiance publiée (https://www.frem.sh/.well-known/slsa-trust-root.json), contrôle que la déclaration concerne bien ce dépôt et ce tag, calcule le condensé de chaque fichier que vous indiquez sur votre machine et le compare au condensé signé, et vérifie que l’enveloppe figure dans le journal de transparence. Un fichier que la release ne mentionne pas échoue.

Code de sortieSignification
0PASS
1FAIL (signature, déclaration, condensé d’un fichier, journal, ou attestation refusée)
2erreur d’utilisation
3PENDING : publiée, pas encore signée ; relancez sous peu

Hors ligne, sans aucun réseau, à partir de l’enveloppe et d’une copie de la racine de confiance :

fremforge release verify acme/web v1.2.3 ./web-linux-amd64.tar.gz \
  --offline --envelope=release.intoto.jsonl --trust-root=slsa-trust-root.json

La vérification hors ligne contrôle la signature, la déclaration et vos fichiers ; elle ne peut pas vérifier le statut de l’attestation ni le journal de transparence, et l’indique.

Les données brutes se trouvent à GET /_app/api/v1/orgs/{org}/repos/{repo}/releases/{tag}/attestation (findings:read) : status (pending, signed, failed, unfrozen), les sujets, le manifeste et, une fois signée, des URL de courte durée pour l’enveloppe DSSE et le bundle Sigstore. Seul signed comporte une enveloppe.

Non couvert

  • Les archives de sources générées automatiquement (les .zip et .tar.gz que Forgejo propose pour chaque tag) ne sont pas attestées : leurs octets ne sont pas stables d’une version de git à l’autre. Le sujet source désigne plutôt le commit du tag ; vérifiez un checkout avec git rev-parse v1.2.3^{commit}.
  • Les releases publiées avant l’activation de l’immuabilité ne sont pas figées et n’ont pas d’attestation de release.

Erreurs

SurfaceForme
API REST de Forgejo422 {"message": "immutable release: …"}
git pushremote: … pre-receive hook declined avec la raison
Registre de conteneurs403 {"errors":[{"code":"DENIED", …}]}
API fremforgenot-enabled, invalid-value, invalid-repo, forgejo-error, not-found