Hoppa till huvudinnehållet

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:

ÄndringTillåten?
Redigera titeln, release-anteckningarna, flaggan för förhandsversionJa
Ändra taggnamnet eller mål-commitenNej
Flytta taggen med git push --force, eller ta bort den med git push --deleteNej
Göra om releasen till ett utkastNej
Lägga till, byta namn på, ersätta eller ta bort en filNej
Ta bort releasenJa, 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_ID

Release-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ägeFryst
off (standard)ingenting: varje tagg kan skrivas över
semverversionstaggar: v1.2.3, 1.2.3, 1.2.3-rc.1; latest, 1, 1.2, main, sha-… förblir flyttbara
allvarje 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.

ÄndringTillåten?
Ladda upp en ny versionJa
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 byteNej
Ta bort en version, eller en av dess filerJa
Publicera en borttagen version igen, med vilka byte som helstNej: versionsnamnet förblir pensionerat
Ladda upp en borttagen fil igen med andra byteNej

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 -SNAPSHOT och maven-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:

PostHå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
*-nightlyvarje 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/immutability

Paketofö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/immutability

Ett 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/web

Pull-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>, med gitCommit.

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

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

AvslutningskodBetydelse
0PASS
1FAIL (signaturen, påståendet, en fils digest, loggen, eller attesteringen nekades)
2användningsfel
3PENDING: 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.json

Offlineverifiering 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 .zip och .tar.gz som 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 med git rev-parse v1.2.3^{commit}.
  • Releaser som publicerades innan oföränderlighet slogs på är inte frysta och har ingen release-attestering.

Fel

YtaForm
Forgejos REST-API422 {"message": "immutable release: …"}
git pushremote: … pre-receive hook declined med orsaken
Container-registry403 {"errors":[{"code":"DENIED", …}]}
fremforge-APInot-enabled, invalid-value, invalid-repo, forgejo-error, not-found