Naar de hoofdinhoud

Onveranderlijke releases en releaseattestatie

Een gepubliceerde release is een belofte: deze bestanden, gebouwd uit deze commit, onder deze tag. Zonder onveranderlijkheid kan die belofte achteraf ongemerkt worden gebroken: een tag die naar een andere commit wordt verlegd, een asset dat wordt vervangen door een andere binary, een release die wordt verwijderd en onder dezelfde naam opnieuw wordt aangemaakt. Onveranderlijke releases maken elk daarvan onmogelijk, en met een releaseattestatie kan iedereen op de eigen machine controleren dat wat hij heeft gedownload ook is wat er werd gepubliceerd.

Wat er wordt bevroren

Als onveranderlijke releases aanstaan, wordt een release bevroren op het moment dat ze wordt gepubliceerd. Vanaf dan:

WijzigingToegestaan?
De titel, de release notes of de pre-release-markering aanpassenJa
De tagnaam of de doelcommit wijzigenNee
De tag verplaatsen met git push --force, of verwijderen met git push --deleteNee
De release terugzetten naar conceptNee
Een asset toevoegen, hernoemen, vervangen of verwijderenNee
De release verwijderenJa, maar de tagnaam kan in die repository nooit meer worden gebruikt

Elke ingang handhaaft dezelfde regel: de webinterface, de REST API (HTTP 422 met een melding die begint met immutable release:) en git zelf (de push wordt door de pre-receive hook afgewezen met een melding die de tag noemt).

Bevriezen werkt alleen vooruit. De instelling aanzetten bevriest niets wat al was gepubliceerd, en haar later uitzetten ontdooit niets. Moet een bevroren release echt worden gewijzigd, neem dan contact op met support: support kan het bevriezen van die ene release opheffen, met een schriftelijke reden die in je auditlog verschijnt.

Publiceer assets voordat je de release publiceert

Een gepubliceerde onveranderlijke release accepteert geen nieuwe assets, dus de volgorde is belangrijk. Maak de release aan als concept, upload de assets en publiceer haar dan:

# 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

Releasetools die eerst een gepubliceerde release aanmaken en daarna uploaden, mislukken bij de upload met 422. Assets die alleen externe links zijn, kunnen geen deel uitmaken van een onveranderlijke release, omdat er geen opgeslagen bytes zijn om een digest van te nemen; publiceren wordt dan geweigerd.

Elk asset draagt nu zijn digest in de API, net als bij GitHub: "digest": "sha256:<hex>", en een onveranderlijke release meldt "immutable": true.

Containertags

Hetzelfde beleid kan tags van containerimages in de registry bevriezen:

ModusBevroren
off (standaard)niets: elke tag kan worden overschreven
semverversietags: v1.2.3, 1.2.3, 1.2.3-rc.1; latest, 1, 1.2, main, sha-… blijven verplaatsbaar
allelke tag behalve de verplaatsbare die je opgeeft (latest, edge-*, …)

Een push van een bevroren tag die hem naar een andere digest laat wijzen, wordt geweigerd met de registryfout DENIED (HTTP 403). Hetzelfde image opnieuw pushen mag wel, zodat een opnieuw gestarte CI-job niet breekt. Een bevroren tag verwijderen wordt geweigerd, en opruimregels voor pakketten selecteren er nooit een; bevroren tags tellen nog wel mee voor je opslagquotum. De handtekening- en attestatietags die cosign schrijft (sha256-<digest>.sig, .att, .sbom) zijn altijd verplaatsbaar. Containertags worden beoordeeld tegen het huidige beleid, dus de modus uitzetten is de manier om er bewust een te verplaatsen of te verwijderen.

Pakketten

Gepubliceerde pakketversies zijn onveranderlijk op fremforge: npm, Maven, PyPI, NuGet, Cargo, Go, Helm, Conan, Composer, RubyGems, Pub, Swift, Conda, CRAN, Debian, RPM, Alpine, Arch, Chef, Vagrant en generieke pakketten. Dit staat standaard aan voor elke organisatie. Containers volgen in plaats daarvan de modus voor containertags hierboven.

WijzigingToegestaan?
Een nieuwe versie uploadenJa
Een nieuw bestand toevoegen aan een bestaande versie (een extra wheel voor een platform, een Maven-classifier-jar)Ja
Een bestand opnieuw uploaden met dezelfde bytes (een opnieuw gestarte CI-job)Ja, verandert niets
Een bestand van een gepubliceerde versie vervangen door andere bytesNee
Een versie, of een van haar bestanden, verwijderenJa
Een verwijderde versie opnieuw publiceren, met welke bytes dan ookNee: de versienaam blijft buiten gebruik
Een verwijderd bestand opnieuw uploaden met andere bytesNee

Een geweigerde upload antwoordt met HTTP 409 en een melding die begint met immutable package:, en de responseheader X-Fremforge-Immutable zegt waarom: overwrite, reupload of retired. Elke weigering wordt in je auditlog geschreven als package.immutable.refused, met het pakket, de versie, het bestand, beide SHA-256-digests en wie het probeerde. Ze verschijnt ook in de toegangslog van de registry als geblokkeerde upload.

De meeste registries weigeren al om een versie twee keer te publiceren. Wat hier verandert:

  • Helm en Conan lieten een versie vroeger ter plekke overschrijven.
  • In elke registry kon een versie worden verwijderd en daarna opnieuw worden gepubliceerd met andere inhoud. Beide worden nu geweigerd.

Altijd veranderlijk:

  • Maven-versies -SNAPSHOT en bestanden maven-metadata.xml, die bewust worden herschreven.
  • De indexbestanden van repository’s die de registries voor Debian, RPM, Alpine en Arch bijhouden.

Wil je andere versies veranderlijk houden, bijvoorbeeld een generiek pakket dat als CI-cache dient, geef ze dan op als uitzonderingen in de vorm [<type>:][<name>@]<version>, waarbij * en ? als wildcards werken:

ItemHoudt veranderlijk
generic:ci-cache@*elke versie van het generieke pakket ci-cache
npm:@acme/ui@0.0.0-dev*ontwikkelbuilds van één npm-pakket
*-nightlyde versies *-nightly van elk pakket

Een organisatie kan onveranderlijkheid van pakketten ook helemaal uitzetten, of terugzetten naar de standaard van het platform (inherit). De instelling wordt beoordeeld op het moment van schrijven, en de namen van versies die zijn verwijderd terwijl ze uitstond, worden nog steeds onthouden. Haar aanzetten beschermt dus ook versies die eerder zijn verwijderd.

Aanzetten

Eigenaren van een organisatie: Beveiliging › Beleid › Onveranderlijke releases in de console (/<org>/_admin/security/policies/immutable-releases), of via de API met een token dat policy:write heeft (voor het lezen van de instellingen is findings:read nodig):

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

Onveranderlijkheid van pakketten gebruikt hetzelfde endpoint. packages is inherit (de standaard van het platform volgen, en die staat aan), on of off, en package_mutable_versions bevat de uitzonderingen. Het antwoord meldt ook 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

Een repository kan de organisatie overschrijven: on bevriest daar altijd releases, ook als de standaard van de organisatie uitstaat; off zegt zich af, en wordt alleen gerespecteerd als de organisatie afmelden toestaat; inherit volgt de organisatie.

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-mirrors worden nooit bevroren: hun tags horen bij de repository die ze spiegelen.

De releaseattestatie

Voor elke onveranderlijke release ondertekent fremforge één in-toto-verklaring van het type https://in-toto.io/attestation/release/v0.1. De subjects ervan zijn:

  • elk asset, op naam en SHA-256 (berekend tijdens het uploaden van het bestand);
  • de bron, git+https://frem.sh/<org>/<repo>@refs/tags/<tag>, op gitCommit.

De verklaring komt uit een manifest dat Forgejo schrijft in dezelfde transactie die de release publiceert, en wordt vóór het ondertekenen opnieuw gecontroleerd tegen de live release. Ze wordt ondertekend met dezelfde sleutels als elke andere attestatie van fremforge (een Ed25519-DSSE-handtekening, plus een sleutelloze Sigstore-bundel van fremforges eigen Fulcio en timestamp authority) en opgenomen in de transparantielog. De attestatie wordt geleverd door de API en de CLI; ze wordt nooit aan de release zelf gehangen.

Ondertekenen vertraagt nooit een publicatie. Een release wordt eerst gepubliceerd en kort daarna ondertekend, dus een korte tijd is haar attestatie pending. Pending is geen geslaagde controle: verificatie meldt PENDING en eindigt met een eigen exitcode.

Een release verifiëren

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

Het commando haalt de attestatie op, verifieert de handtekening tegen de gepubliceerde trust root (https://www.frem.sh/.well-known/slsa-trust-root.json), controleert dat de verklaring voor deze repository en tag is, hasht elk bestand dat je noemt op je eigen machine en vergelijkt het met de ondertekende digest, en controleert dat de envelop in de transparantielog staat. Een bestand dat de release niet noemt, faalt.

ExitcodeBetekenis
0PASS
1FAIL (handtekening, verklaring, de digest van een bestand, de log, of de attestatie werd geweigerd)
2gebruiksfout
3PENDING: gepubliceerd, nog niet ondertekend; probeer het zo opnieuw

Offline, zonder enig netwerk, met de envelop en een kopie van de trust root:

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

Offline verificatie controleert de handtekening, de verklaring en je bestanden; de status van de attestatie en de transparantielog kan ze niet controleren, en dat meldt ze ook.

De ruwe gegevens staan op GET /_app/api/v1/orgs/{org}/repos/{repo}/releases/{tag}/attestation (findings:read): status (pending, signed, failed, unfrozen), de subjects, het manifest en, zodra ondertekend, kortlevende URL’s voor de DSSE-envelop en de Sigstore-bundel. Alleen signed bevat een envelop.

Niet gedekt

  • De automatisch gegenereerde bronarchieven (de .zip en .tar.gz die Forgejo bij elke tag aanbiedt) worden niet geattesteerd: hun bytes zijn niet stabiel tussen git-versies. Het bronsubject noemt in plaats daarvan de commit van de tag; verifieer een checkout met git rev-parse v1.2.3^{commit}.
  • Releases die zijn gepubliceerd voordat onveranderlijkheid werd aangezet, zijn niet bevroren en hebben geen releaseattestatie.

Fouten

IngangVorm
Forgejo REST API422 {"message": "immutable release: …"}
git pushremote: … pre-receive hook declined met de reden
Containerregistry403 {"errors":[{"code":"DENIED", …}]}
fremforge-APInot-enabled, invalid-value, invalid-repo, forgejo-error, not-found