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:
| Wijziging | Toegestaan? |
|---|---|
| De titel, de release notes of de pre-release-markering aanpassen | Ja |
| De tagnaam of de doelcommit wijzigen | Nee |
De tag verplaatsen met git push --force, of verwijderen met git push --delete | Nee |
| De release terugzetten naar concept | Nee |
| Een asset toevoegen, hernoemen, vervangen of verwijderen | Nee |
| De release verwijderen | Ja, 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_IDReleasetools 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:
| Modus | Bevroren |
|---|---|
off (standaard) | niets: elke tag kan worden overschreven |
semver | versietags: v1.2.3, 1.2.3, 1.2.3-rc.1; latest, 1, 1.2, main, sha-… blijven verplaatsbaar |
all | elke 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.
| Wijziging | Toegestaan? |
|---|---|
| Een nieuwe versie uploaden | Ja |
| 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 bytes | Nee |
| Een versie, of een van haar bestanden, verwijderen | Ja |
| Een verwijderde versie opnieuw publiceren, met welke bytes dan ook | Nee: de versienaam blijft buiten gebruik |
| Een verwijderd bestand opnieuw uploaden met andere bytes | Nee |
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
-SNAPSHOTen bestandenmaven-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:
| Item | Houdt veranderlijk |
|---|---|
generic:ci-cache@* | elke versie van het generieke pakket ci-cache |
npm:@acme/ui@0.0.0-dev* | ontwikkelbuilds van één npm-pakket |
*-nightly | de 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/immutabilityOnveranderlijkheid 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/immutabilityEen 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/webPull-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>, opgitCommit.
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.txtHet 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.
| Exitcode | Betekenis |
|---|---|
0 | PASS |
1 | FAIL (handtekening, verklaring, de digest van een bestand, de log, of de attestatie werd geweigerd) |
2 | gebruiksfout |
3 | PENDING: 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.jsonOffline 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
.zipen.tar.gzdie 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 metgit rev-parse v1.2.3^{commit}. - Releases die zijn gepubliceerd voordat onveranderlijkheid werd aangezet, zijn niet bevroren en hebben geen releaseattestatie.
Fouten
| Ingang | Vorm |
|---|---|
| Forgejo REST API | 422 {"message": "immutable release: …"} |
| git push | remote: … pre-receive hook declined met de reden |
| Containerregistry | 403 {"errors":[{"code":"DENIED", …}]} |
| fremforge-API | not-enabled, invalid-value, invalid-repo, forgejo-error, not-found |