Oföränderliga releaser och release-attestering
En publicerad release är ett löfte: de här filerna, byggda från den här commiten, under den här taggen. Utan oföränderlighet kan det löftet tyst brytas i efterhand: en tagg som flyttas till en annan commit, en fil som byts mot en annan binärfil, en release som tas bort och skapas igen under samma namn. Oföränderliga releaser gör allt detta omöjligt, och en release-attestering låter vem som helst kontrollera, på sin egen maskin, att det de laddade ned är det som publicerades.
Vad som fryses
När oföränderliga releaser är påslaget fryses en release i samma ögonblick som den publiceras. Därefter gäller:
| Ändring | Tillåten? |
|---|---|
| Redigera titeln, release-anteckningarna, flaggan för förhandsversion | Ja |
| Ändra taggnamnet eller mål-commiten | Nej |
Flytta taggen med git push --force, eller ta bort den med git push --delete | Nej |
| Göra om releasen till ett utkast | Nej |
| Lägga till, byta namn på, ersätta eller ta bort en fil | Nej |
| Ta bort releasen | Ja, men dess taggnamn kan aldrig användas igen i det repot |
Varje yta tillämpar samma regel: webbgränssnittet, REST-API:t (HTTP 422
med ett meddelande som börjar med immutable release:) och git självt (pushen
avvisas av pre-receive-hooken med ett meddelande som anger taggen).
Frysningen gäller bara framåt. Att slå på inställningen fryser inget som redan publicerats, och att slå av den senare tinar inte upp något. Om en fryst release verkligen måste ändras, kontakta supporten: supporten kan häva frysningen för just den releasen, med en skriftlig motivering som visas i din granskningslogg.
Ladda upp filerna innan du publicerar releasen
En publicerad oföränderlig release tar inte emot nya filer, så ordningen spelar roll. Skapa releasen som ett utkast, ladda upp filerna och publicera den sedan:
# 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_IDRelease-verktyg som skapar en publicerad release och laddar upp efteråt misslyckas
vid uppladdningen med 422. Filer som bara är externa länkar kan inte
ingå i en oföränderlig release, eftersom det inte finns några lagrade byte att ta en
digest av; att publicera en sådan nekas.
Varje fil har nu sin digest i API:t, precis som hos GitHub:
"digest": "sha256:<hex>", och en oföränderlig release rapporterar "immutable": true.
Container-taggar
Samma policy kan frysa taggar för container-images i registryt:
| Läge | Fryst |
|---|---|
off (standard) | ingenting: varje tagg kan skrivas över |
semver | versionstaggar: v1.2.3, 1.2.3, 1.2.3-rc.1; latest, 1, 1.2, main, sha-… förblir flyttbara |
all | varje tagg utom de flyttbara som du listar (latest, edge-*, …) |
En push av en fryst tagg som pekar den mot en annan digest nekas
med registry-felet DENIED (HTTP 403). Att pusha samma image igen
är tillåtet, så att ett omkört CI-jobb inte går sönder. Att ta bort en fryst tagg
nekas, och regler för paketrensning väljer aldrig en sådan; frysta taggar räknas ändå
mot din lagringskvot. De signatur- och attesteringstaggar som cosign skriver
(sha256-<digest>.sig, .att, .sbom) är alltid flyttbara. Container-taggar
utvärderas mot den aktuella policyn, så att stänga av läget är sättet att flytta
eller ta bort en tagg avsiktligt.
Paket
Publicerade paketversioner är oföränderliga på fremforge: npm, Maven, PyPI, NuGet, Cargo, Go, Helm, Conan, Composer, RubyGems, Pub, Swift, Conda, CRAN, Debian, RPM, Alpine, Arch, Chef, Vagrant och generiska paket. Det är påslaget för alla organisationer som standard. Containrar följer i stället läget för container-taggar ovan.
| Ändring | Tillåten? |
|---|---|
| Ladda upp en ny version | Ja |
| Lägga till en ny fil i en befintlig version (ytterligare en wheel för en plattform, en Maven-classifier-jar) | Ja |
| Ladda upp en fil igen med samma byte (ett omkört CI-jobb) | Ja, ändrar ingenting |
| Ersätta en fil i en publicerad version med andra byte | Nej |
| Ta bort en version, eller en av dess filer | Ja |
| Publicera en borttagen version igen, med vilka byte som helst | Nej: versionsnamnet förblir pensionerat |
| Ladda upp en borttagen fil igen med andra byte | Nej |
En nekad uppladdning besvaras med HTTP 409 och ett meddelande som börjar med
immutable package:, och svarshuvudet X-Fremforge-Immutable anger
varför: overwrite, reupload eller retired. Varje nekande skrivs till din
granskningslogg som package.immutable.refused, med paketet, versionen,
filen, båda SHA-256-digesterna och vem som försökte. Det visas också i
åtkomstloggen för registry som en blockerad uppladdning.
De flesta registry nekar redan att publicera en version två gånger. Det som ändras här:
- Helm och Conan lät tidigare en version skrivas över på plats.
- I alla registry kunde en version tas bort och sedan publiceras igen med annat innehåll. Båda nekas nu.
Alltid föränderliga:
- Maven-versioner med
-SNAPSHOTochmaven-metadata.xml-filer, som skrivs om av design. - De indexfiler för repot som registry för Debian, RPM, Alpine och Arch underhåller.
För att hålla andra versioner föränderliga, till exempel ett generiskt paket som används som
CI-cache, listar du dem som undantag i formen
[<type>:][<name>@]<version>, där * och ? fungerar som jokertecken:
| Post | Håller föränderligt |
|---|---|
generic:ci-cache@* | varje version av det generiska paketet ci-cache |
npm:@acme/ui@0.0.0-dev* | utvecklingsbyggen av ett npm-paket |
*-nightly | varje pakets *-nightly-versioner |
En organisation kan också stänga av paketoföränderlighet helt (off), eller återgå till
plattformens standard (inherit). Inställningen utvärderas när en skrivning
sker, och namnen på versioner som tagits bort medan den var avstängd är fortfarande
ihågkomna. Att slå på den skyddar alltså även versioner som tagits bort
tidigare.
Slå på det
Organisationsägare: Säkerhet › Policyer › Oföränderliga releaser i konsolen
(/<org>/_admin/security/policies/immutable-releases), eller API:t med en token som har
policy:write (för att läsa inställningarna krävs 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/immutabilityPaketoföränderlighet använder samma endpoint. packages är inherit (följ
plattformens standard, som är påslagen), on eller off, och
package_mutable_versions listar undantagen. Svaret rapporterar också
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/immutabilityEtt repo kan åsidosätta organisationen: on fryser alltid releaser
där, även när organisationens standard är avstängd; off avregistrerar repot, och
respekteras bara när organisationen tillåter avregistrering; inherit följer
organisationen.
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/webPull-speglar fryses aldrig: deras taggar tillhör det repo de speglar.
Release-attesteringen
För varje oföränderlig release signerar fremforge ett
in-toto-påstående av typen
https://in-toto.io/attestation/release/v0.1. Dess subjekt är:
- varje fil, med namn och SHA-256 (beräknad medan filen laddades upp);
- källan,
git+https://frem.sh/<org>/<repo>@refs/tags/<tag>, medgitCommit.
Påståendet hämtas från ett manifest som Forgejo skriver i samma transaktion som publicerar releasen, och kontrolleras igen mot den aktuella releasen innan det signeras. Det signeras med samma nycklar som alla andra fremforge-attesteringar (en Ed25519 DSSE-signatur, plus en nyckellös Sigstore-bunt från fremforges egen Fulcio och tidsstämpelmyndighet) och förs in i transparensloggen. Attesteringen levereras av API:t och CLI:t; den bifogas aldrig till själva releasen.
Signeringen fördröjer aldrig en publicering. En release publiceras först och signeras
kort därefter, så under en kort stund är dess attestering pending. Pending är
inte godkänt: verifieringen säger PENDING och avslutar med en egen kod.
Verifiera en release
fremforge release verify acme/web v1.2.3 ./web-linux-amd64.tar.gz ./checksums.txtKommandot hämtar attesteringen, verifierar signaturen mot den publicerade
förtroenderoten (https://www.frem.sh/.well-known/slsa-trust-root.json), kontrollerar att
påståendet gäller det här repot och den här taggen, hashar varje fil du anger på din
maskin och jämför med den signerade digesten, och kontrollerar att kuvertet finns i
transparensloggen. En fil som releasen inte anger misslyckas.
| Avslutningskod | Betydelse |
|---|---|
0 | PASS |
1 | FAIL (signaturen, påståendet, en fils digest, loggen, eller attesteringen nekades) |
2 | användningsfel |
3 | PENDING: publicerad, inte signerad än; kör igen om en stund |
Offline, helt utan nätverk, med kuvertet och en kopia av förtroenderoten:
fremforge release verify acme/web v1.2.3 ./web-linux-amd64.tar.gz \
--offline --envelope=release.intoto.jsonl --trust-root=slsa-trust-root.jsonOfflineverifiering kontrollerar signaturen, påståendet och dina filer; den kan inte kontrollera attesteringens status eller transparensloggen, och säger det.
Rådatan finns på
GET /_app/api/v1/orgs/{org}/repos/{repo}/releases/{tag}/attestation
(findings:read): status (pending, signed, failed, unfrozen),
subjekten, manifestet och, när den är signerad, kortlivade URL:er till DSSE-kuvertet
och Sigstore-bunten. Bara signed har ett kuvert.
Omfattas inte
- De automatiskt genererade källarkiven (de
.zipoch.tar.gzsom Forgejo erbjuder för varje tagg) attesteras inte: deras byte är inte stabila mellan git-versioner. Källsubjektet anger i stället taggens commit; verifiera en utcheckning medgit rev-parse v1.2.3^{commit}. - Releaser som publicerades innan oföränderlighet slogs på är inte frysta och har ingen release-attestering.
Fel
| Yta | Form |
|---|---|
| Forgejos REST-API | 422 {"message": "immutable release: …"} |
| git push | remote: … pre-receive hook declined med orsaken |
| Container-registry | 403 {"errors":[{"code":"DENIED", …}]} |
| fremforge-API | not-enabled, invalid-value, invalid-repo, forgejo-error, not-found |