Repository navigation
Releases: forgepkg/forge
Release list
v2.1.2
Fixed
- A first pull of a large layer through a Docker proxy could fail with
internal errorwhile the layer was stored all the same, and the next pull
served it: the server loggedregistry: blob readwithNoSuchKey. When the
answer to an upload'sCompleteMultipartUpload(8 MiB and more,
s3.multipart_threshold) was lost, the S3 client sent it again, and
versitygw answers a complete that is still under way with success at once,
before the object is in place; the proxy then read the layer back too early.
An upload that completes counts as stored only once its object can be read,
waited for as long as an answer from S3.
(#682)
Image
ghcr.io/forgepkg/forge on GitHub Packages.
Built from commit b2d961b, which v2.1.2 points to. The cosign verify commands below accept only a signature by this repository's release workflow for that tag and that commit.
One image for linux/amd64, linux/arm64: the digest below is that of the index of the platforms' images (listed under it), from which docker pull and Kubernetes take the node's platform. The index and each platform's image are signed: the cosign verify command passes for each of these digests.
docker pull ghcr.io/forgepkg/forge:2.1.2
# digest: sha256:c2258feaec9aebcb86f8df945ea5ec381c78cb3e09711182e71fded929e572a5
# linux/amd64: sha256:bac6eeb7802f7b82f371bc7df79527308a6690c4005249eb5091ff2571921794
# linux/arm64: sha256:42af8127d6dc5541063fda334fe561dfcb4ea8bd7833b1163ddeabf4f1c5b624
cosign verify ghcr.io/forgepkg/forge@sha256:c2258feaec9aebcb86f8df945ea5ec381c78cb3e09711182e71fded929e572a5 \
--certificate-identity https://github.com/forgepkg/forge/.github/workflows/release.yml@refs/tags/v2.1.2 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-sha b2d961b849a8ce31da5fc343d5a8fb2595c7b92a \
--certificate-github-workflow-trigger push
Helm chart
oci://ghcr.io/forgepkg/charts/forge on GitHub Packages.
Deploys exactly the image above (by digest).
helm install forge oci://ghcr.io/forgepkg/charts/forge --version 2.1.2
# digest: sha256:192e2586ebd1a2b5eb76e3a6ce5d1c1529e414c105b9a387cf81d136d670bd7a
cosign verify ghcr.io/forgepkg/charts/forge@sha256:192e2586ebd1a2b5eb76e3a6ce5d1c1529e414c105b9a387cf81d136d670bd7a \
--certificate-identity https://github.com/forgepkg/forge/.github/workflows/release.yml@refs/tags/v2.1.2 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-sha b2d961b849a8ce31da5fc343d5a8fb2595c7b92a \
--certificate-github-workflow-trigger push
v2.1.1
Features
- The upstream metadata documents that proxies hold at once while reading them
share one budget per instance,proxy.max_metadata_memory_bytes(256 MiB by
default). A read that would go over it waits for others to finish, up to
proxy.metadata_timeout_seconds, then answers 503 withRetry-After, where
before a restore against a cold proxy could grow the process past its memory
limit and have it killed. The deployment guide's
Memory section relates it to the pod's limit.
(#668) - forge-server allocates memory with jemalloc instead of glibc's
malloc.
Memory freed after a burst of requests, mostly large proxy metadata
documents, goes back to the system after about 10 s. With glibc it mostly
stayed with the process, whose resident memory climbed under load toward the
pod's memory limit and hardly came down. jemalloc takes its run-time options
from_RJEM_MALLOC_CONF(Memory allocator in the deployment guide);
glibc'sMALLOC_*variables no longer apply.
(#669) - Forge can publish patch releases of an older line, from its maintenance
branch, whilemainheads for the next minor version: 2.1.1 ships 2.1.0's
fixes without 2.2.0's changes. The image's:<major>.<minor>tag follows a
line's newest patch, and:latestthe newest release of all.
(#674)
Fixed
- An npm proxy parsed the whole upstream packument into memory for every
operation of a packument read, several times its size each: reading the full
packuments oftypescript,@types/node,react-dom,tailwindcssand
viteat once took over 600 MiB, and OOMKilled pods with a 1 GiB limit. Each
operation now reads only what it needs of it, a quarter of that. A full
packument larger thanproxy.max_metadata_bytes(vite's, 39 MB) no longer
answers 500: the full packument Forge serves leaves out the upstream's publish
times and the README, and a version's manifest comes from the version's own
document. A document over that limit is not downloaded again until the
metadata TTL has passed.
(#671)
Security
- The web UI's router serializes its state with
seroval, which before 1.6.11
let serialized output inject code (GHSA-f86p-h4j2-7qgj, GHSA-9wx9-5pv9-3349);
the web UI now gets 1.6.12.
(#677)
Image
ghcr.io/forgepkg/forge on GitHub Packages.
Built from commit 5e06a1a, which v2.1.1 points to. The cosign verify commands below accept only a signature by this repository's release workflow for that tag and that commit.
One image for linux/amd64, linux/arm64: the digest below is that of the index of the platforms' images (listed under it), from which docker pull and Kubernetes take the node's platform. The index and each platform's image are signed: the cosign verify command passes for each of these digests.
docker pull ghcr.io/forgepkg/forge:2.1.1
# digest: sha256:5fab42d1c9545984500615366b5eef2007e581c0ecbd05dadff36151134a4a59
# linux/amd64: sha256:de0f7d31103d3609ccc6b648013b62e0fb2a2f3eeef788785f0c403a10e5805a
# linux/arm64: sha256:763ee55d5d68bbb6f2a77afd16f01015eb0bc91112a7d6f586174d2943064fa0
cosign verify ghcr.io/forgepkg/forge@sha256:5fab42d1c9545984500615366b5eef2007e581c0ecbd05dadff36151134a4a59 \
--certificate-identity https://github.com/forgepkg/forge/.github/workflows/release.yml@refs/tags/v2.1.1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-sha 5e06a1acf07f97ddd6c8a5bf87f2126b9f28a261 \
--certificate-github-workflow-trigger push
Helm chart
oci://ghcr.io/forgepkg/charts/forge on GitHub Packages.
Deploys exactly the image above (by digest).
helm install forge oci://ghcr.io/forgepkg/charts/forge --version 2.1.1
# digest: sha256:b0b2d93ff6cc0757040f82ca7e073462bebd5f357eb24499dc6cf3347e8c22bd
cosign verify ghcr.io/forgepkg/charts/forge@sha256:b0b2d93ff6cc0757040f82ca7e073462bebd5f357eb24499dc6cf3347e8c22bd \
--certificate-identity https://github.com/forgepkg/forge/.github/workflows/release.yml@refs/tags/v2.1.1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-sha 5e06a1acf07f97ddd6c8a5bf87f2126b9f28a261 \
--certificate-github-workflow-trigger push
v2.1.0
Upgrade notes
- The anonymous mode
allowlistand itsanonymous_ip_allowlistare removed;
the anonymous modes areoffandall. An instance whose stored mode is
allowlistcomes up with anonymous accessoff, and the settings API
refuses"anonymous_mode": "allowlist"andanonymous_ip_allowlist. To keep
letting a network read without credentials, admit it at the load balancer
instead: create a service account under Admin → Service accounts in a
trusted-header realm, give it read grants on the repositories the network
read anonymously (those that allow anonymous reads, and those the
anonymousrole has grants on), and have the load balancer name that
account for the networks that were on the allowlist. See the deployment
guide, "Trusted-header realms".
(#507) - Stop every 2.0.0 instance before the first 2.1.0 instance starts,
rather than rolling the upgrade. The first 2.1.0 instance migrates the
database. While 2.0.0 instances still serve, the migration can wait behind
a 2.0.0 database connection left inside a transaction, and every sign-in
and token check of every instance then waits behind it, for up to 30
minutes. An OIDC sign-in on a 2.0.0 instance can also link its identity to
an account of a trusted-header realm meanwhile. With the Helm chart,
upgrade with--set strategy.type=Recreateor scale the deployment to zero
first; with the Kubernetes manifests, scale theforgedeployment to zero
before applying 2.1.0.
(#615)
Features
- OpenTelemetry metrics, traces and logs, exported over OTLP/HTTP with the
official OpenTelemetry SDK.[telemetry]turns each signal on (all are off
by default), and the standardOTEL_*variables say where they go. The
metrics cover requests by surface, proxy lookups by outcome with the upstream
requests and bytes, S3 operations and the circuit breaker, the blob cache's
counters, staging space, the outbox backlog, cache hits, the connection pool,
every background worker's passes and restarts, retention, sign-ins, and an
inventory of repositories, versions and stored bytes; no attribute carries a
package, version, image, user or path, andtelemetry.per_repository = false
takes the repository names off. A request is a trace, joining the caller's
from itstraceparent, with the proxy lookup, the upstream requests (which
carry the trace on) and the S3 operations beneath it; each worker pass is a
trace of its own. A span's URL holds no query string, and the secret in the
path of an npm web login is recorded as{secret}. Exported log records
carry the trace they were written in. The Helm chart (telemetry.*) and the
kustomize componentcomponents/opentelemetryturn it on, and
deploy/monitoringhas an example Collector configuration, a Grafana
dashboard and alerting rules. See the deployment guide's Telemetry section.
(#178) - Trusted-header realms: a
trusted_headerrealm (Admin → Authentication)
authenticates a request as the account a header of the load balancer or SSO
proxy in front of Forge names, when the proxy proves itself with the realm's
secret in a second header. The headers count only from a peer in
auth.trusted_proxy_cidrs, for a request naming one of Forge's own host
names, and only when the request has no credential of its own. A realm names
only its own accounts (created under Admin → Users, or on the first
request withauto_provision) and never makes anyone an administrator: its
accounts never hold theadminrole. Headers with a wrong secret or a name
it may not use (a deactivated account's) name nobody: the request is served
as one without credentials, never anonymously, so its own credential and
signing in still work. A load balancer can so admit a network as an account
with its own grants. See the deployment guide,
"Trusted-header realms".
(#506) - Service accounts: an account for a machine or a network that nobody signs in
to, such as a CI pipeline, a build agent or a network the load balancer
admits (Admin → Service accounts, orPOST /admin/userswith
"kind": "service"). A service account has a description, roles and grants,
and no password, invitation or email. The login screen, password Basic,
credential forwarding and OIDC all refuse it, and it never holds theadmin
role. It authenticates with the API tokens an administrator issues for it,
and through the one trusted-header realm it may belong to, which alone may
name it. Deactivating it stops both.GET /admin/usersgainskind,
description,realmandlast_used_atin its views, and a?kind=filter.
The accounts that exist are users. See the deployment guide, "Service
accounts".
(#508) - Administrators delete images from the web UI: on the image page of a hosted
docker repository, the whole image, a tag, a manifest (with the platform
manifests and referrers only it kept) or a Helm chart's version. The admin
API has it asDELETE /admin/repositories/<name>/images?image=<image>, with
reference=<tag|digest>for a tag or a manifest. A hosted docker
repository, deleted only once it holds no image, can now be emptied without
the registry API.
(#509)
Fixed
- Of two first pushes of the same package version that arrived at the same
moment (two CI jobs, or two replicas), the one that lost the race failed
with 409 even where the repository allows redeploys. It now replaces the
version, as it would have a moment later. This holds for NuGet, npm and
the other package formats; a repository without redeploys still answers
409.
(#226) - The retention preview of a package or proxy repository evaluated its rules
at the Forge host's clock, while a run evaluates them at the database's.
With the two clocks apart, a version near anolder_than_days,
not_downloaded_for_daysornot_pulled_for_daysboundary could be shown
as kept and then be deleted by the run. The preview now evaluates at the
database's clock, as image and file previews already did.
(#227) - A proxy operation that reads several upstream documents (a version listing,
dating the package delay of a package with many versions) kept every
document it read until it answered: up to 256 of them, each up to
proxy.max_metadata_bytes. An upstream serving large pages could make a few
such operations hold gigabytes. One operation now keeps at most 4 ×
proxy.max_metadata_bytesof documents. The documents past that are not
read, and the format answers with the others: NuGet leaves the versions of
an unread registration page to the listing.
(#230) - The page of an image with many tags and manifests (a CI that tags every
build, or a proxy of an image with thousands of tags upstream) took seconds
to answer and held a server thread all the while: it compared every tag
with every manifest, 24 s for 50,000 of them. It now answers in
milliseconds, and lists the newest 1,000 tags and 1,000 manifests, saying
how many the image has (tags_totalandmanifests_totalin
GET /browse/images/<repo>/<image>).
(#240) - The browse of a docker group listed the images and tags of every member the
reader could read, ignoring the group's guards. An image a proxy member had
cached under one of the group'sreserved_prefixes, or of a name an owning
hosted member has (prefer_hosted), was listed with adocker pull
command that the group then refused, which is exactly the
dependency-confusion image the guards keep out. The group's own content
selector was not applied either. The images list, an image's page and a
chart's page now show what a pull through the group serves. A manifest of
a lower priority member no longer wears a tag that a higher priority
member's manifest answers.
(#241) - Browse kept listing repositories as they were for up to 30 seconds after an
administrator created, renamed, deleted or took one offline, or changed a
group's members, under Manage: a renamed repository was still listed
under its old name, which then showed as empty. Those changes now refresh
what Browse shows.
(#244) - A docker proxy's images showed when Forge first cached them as "Pushed" (the
manifests on an image's page) and "Last pushed" (the images of the
repository), as if pushed to the upstream then. A proxy labels them
"Cached" and "Last cached", as package versions already were.
(#248) - The last run of a package or proxy repository's retention policy counted the
versions it decided on asevaluated("of N" on the Retention screen), not
the versions it inspected. It now counts every candidate version, as the
preview does and as docker and raw runs already did.
(#439) - npm repositories read a package's tarball as npm unpacks it, contiguous-file
entries (type7) included: apackage.jsonof that type after a regular
one is the version's manifest, as it is the one npm installs, and such
entries count indist.fileCount. Forge skipped them, so a tarball could
show other dependencies and install scripts in Forge than run on install.
(#504) - A NuGet package that repeats an entry name was refused, whether pushed or
fetched through a proxy, though...
v2.0.0
A new major release, installed fresh: 2.0.0 does not upgrade 1.0 or 1.1.
Install it on an empty PostgreSQL database and an object store of its own;
docs/deployment.md
lists the required settings. It adds npm and raw (generic file)
repositories, serves every package repository at /repository/<name>/,
keeps Helm charts as OCI artifacts in docker repositories, invites users by
email, and manages roles, access levels and retention in the web UI. Every
format is built in: Docker / OCI, NuGet, npm and raw. Forge is licensed
under the MIT License (up to 1.1.0: the GNU AGPL v3 or a commercial
license), and is published as ghcr.io/forgepkg/forge for linux/amd64
and linux/arm64, with the Helm chart oci://ghcr.io/forgepkg/charts/forge.
Upgrade notes
- 2.0.0 does not upgrade 1.0 or 1.1. There is no upgrade path: 2.0.0 is
installed fresh, with an empty PostgreSQL database and an object-store
bucket (or prefix) of its own. Startup refuses a database created by 1.x or
by a build before 2.0.0 ("the database was created by Forge 1.x or by a
build before 2.0.0, and 2.0.0 does not upgrade it"). Upgrades are from 2.0.0
on.
(#490,
#498)
Features
- Credential forwarding: a realm can keep an argon2id hash of every password
its upstream (e.g. a Nexus) accepts (Keep a password hash of successful
logins,capture_password_hash, off by default; unchecking it deletes the
hashes). Once the hashes are in place the realm can stop asking the upstream
and sign its users in with them (captured_password_only), and its accounts
can be switched to local accounts with the same password, one by one or all
at once (POST /admin/users/{id}/convert-to-local,
POST /admin/realms/{id}/convert-to-local), so the upstream can be retired
without asking every user for a new password. - OIDC login without a client secret: an
oidcrealm can authenticate
at Microsoft Entra with a federated identity credential instead
(client_authentication = "federated_credential"). Forge signs a
short-lived client assertion itself, as an OpenID Connect issuer of its
own (server.public_base_url), publishing
/.well-known/openid-configurationand its keys at
/.well-known/jwks.json. The keys are RSA-2048, shared by every instance
through the database (sealed with[crypto]), and rotated every 30 days,
each new key published two days before it signs. The realm shows the
issuer, subject (<client id>@<realm issuer>) and audience to register.
See DEVELOPMENT.md.
(#302) - Helm:
extraObjectscan also be a map, whose keys only name its entries.
Helm merges a map across values files, so each file can add its own
objects, and a later one replaces an entry by its key or drops it with
null; a list in a later file replaces the whole list. Lists keep
working.
(#288) - Import from Nexus: copy a Nexus 3 hosted repository into a Forge hosted
repository of the same format, one repository at a time: NuGet packages,
and Docker images with their digests, multi-platform indexes, signatures
and SBOMs. In the web UI (a hosted repository's
Import from Nexus) or the admin API
(/admin/repositories/{name}/imports). The source is checked before an
import starts; a dry run reports the counts, the size and the conflicts
without writing. Packages are verified against Nexus' checksums and
written through the normal push path, keep their publish dates, last
downloads and uploaders, and are audited as imports. A version Forge has
with other bytes is skipped and reported, overwritten, or fails the
import, as chosen. An import resumes after a restart, a failure or a
cancel, failed items can be retried on their own, and running it again
copies only what is new. The Nexus password is sealed with the secret
encryption and deleted when the import completes. See
docs/nexus-import.md.
(#183) - npm registries: repositories of
"format": "npm", served at
/repository/<repository>/for npm, pnpm, Yarn and Bun. Hosted ones take
npm publish(the tarball checked against the integrity the client claims
and the version it names, under the repository's deployment policy),
dist-tags andnpm deprecatewith the levelwrite, andnpm unpublish
withdelete. Proxies cacheregistry.npmjs.orgor any registry, with
Basic credentials or a token sent asAuthorization: Bearerto its own
origin only, and serve the upstream's dist-tags, deprecations and publish
times, verified tarballs and the publication delay. Groups merge their
members, take publishes into a writable member, keep reserved scopes
(@acme) away from proxy members and passnpm auditon to their proxy
without the reserved names.npm loginis approved in the browser on a
page of the web UI, or checks a password (yarn npm login), and mints an
API token thatnpm logoutrevokes; npm clients send API tokens as
Authorization: Bearer, which every format plugin's repositories now
accept. Retention never deletes a version a dist-tag names. The web UI
shows a package's dist-tags, deprecated versions and README, and a
repository's.npmrcand.yarnrc.ymllines and install commands. npm is
a built-in format plugin (crates/formats/npm). See
docs/npm.md.
(#41) - Hosted NuGet repositories have a deployment policy, like raw and npm ones:
allow redeploys, refuse them (the default), or read-only, which refuses
every push, unlist and delete.allow_redeploysays the same and follows
it. A NuGet group may have a writable member, a hosted member that pushes,
unlists and deletes through the group go to. The web UI's repository form
offers both.
(#41) - Raw (generic file) repositories, like Nexus' raw-hosted, raw-proxy and
raw-group:"format": "raw"repositories serve arbitrary files under
arbitrary paths at/repository/<repository>/<path>. Uploads are aPUTwith an
API token or a multipart form (Nexus'raw.directory/raw.assetNfields
work); downloads answer oneRange, conditional requests, anETag(the
SHA-256) andLast-Modified, and<file>.sha256,.sha512,.sha1and
.md5from the stored digests, sosha256sum -cchecks a download.
Directories list as HTML or JSON, a page at a time. A hosted repository's
deployment policy allows redeploys, refuses them (the default) or makes it
read-only; files download unless the repository displays theminline, and
are sandboxed either way. Paths with..,., empty segments, backslashes
or encoded slashes are refused. A proxy fetches from any HTTP(S) base URL,
following redirects (GitHub release assets) with its credentials sent to
its own origin only, re-validates cached files aftermax_age_seconds(0:
never), remembers misses, and serves its cached copy while the upstream is
down; a group serves a path from its first member that has it. Content
selectors match paths, retention deletes files by path prefix, age and
downloads or keeps the newest per directory, and the web UI browses a
repository's tree, uploads and deletes files and shows each file's URL and
curlcommands. See docs/raw.md.
(#42) - Helm charts pushed to a docker repository (
helm push) have a page of their
own in the web UI: the chart's versions, thehelm installandhelm pull
commands of the selected one (the newest release by default) with the
helm registry loginto run first, itsChart.yamlfacts (appVersion,
description, home, sources, maintainers, dependencies), its README and its
default values. The browse API serves them at
GET /browse/chart?repo=<name>&image=<image>&digest=<manifest digest>; a
proxy shows them once the chart has been pulled through it. See
docs/docker.md.
(#43) - A proxy's retention rules (
scope: proxy_cached) can keep a working set of
each package:keep_last_nkeeps the newest N cached versions the upstream
lists, andkeep_n_most_downloadedthe N cached versions pulled most
recently (by when they were last pulled, not how often). A version any proxy
rule keeps is never evicted, however long nobody pulls it, so a quiet package
no longer loses every cached version and stays restorable while the upstream
is unreachable. The Retention screen offers both on proxy rules, and a new
proxy policy starts by keeping the newest three versions of each package.
(#59) - Forge runs on arm64: each release publishes
ghcr.io/forgepkg/forgefor
linux/amd64andlinux/arm64as one multi-platform image under the same
tags, so arm64 nodes (AWS Graviton, Azure Cobalt, Ampere, Raspberry Pi) and
Apple Silicon machines pull an image of their own platform, without
emulation. Each platform's image carries its own SBOM and provenance
attestations. The release signs the multi-platform index and each platform's
image, so the release notes'cosign verifycommand passes for each of their
digests, and the Helm chart pins the index digest.cargo xtask docker-build --platform linux/arm64builds the arm64 image from a checkout.
(#186) - Admin → Settings (
/manage/settings) sets who may read without signing
in (the anonymous mode and its IP allo...
v1.1.0
The Docker / OCI registry, format plugins (Helm built in, NuGet ported to
one), the trusted-publishing token exchange, short-lived S3 credentials and a
local blob cache, and the fixes for the findings of the 1.0.0 release review.
The 1.0.0 known limitations still apply, except where an entry below says
otherwise.
Upgrade notes
- Startup refuses a
server.staging_dirthat does not belong to Forge's
user, or that group or others can write unless it is sticky like/tmp.
The container image and the Kubernetes manifests are not affected.
(#127) - Durations above 100 years (
outbox.grace_seconds,
retention.default_run_interval_seconds,retention.lease_seconds,
crypto.cache.unwrapped_key_ttl_secs,
crypto.rotation.dek_max_age_days,crypto.rotation.kek_max_age_days,
and the newdocker.upload_ttl_seconds,
docker.unreferenced_blob_ttl_secondsanddocker.sweep_interval_seconds)
fail validation instead of panicking later.
(#128) - Migration 0032 deletes grants of the
adminaction on a single
repository, which never had an effect, and the grants API refuses them
with 400. Administration stays global.
(#124) - While a credential-forwarding realm is enabled, an account that an OIDC
realm provisions is named<claim>@<realm>. Existing accounts keep their
names. (#123) - Proxy search now calls the upstream's search service, which may live on
another host than the feed (on nuget.org,azuresearch-*.nuget.org).
Where outbound traffic is restricted, allow that host, or proxy search
keeps answering from the cache only.
(#112) - The production kustomize overlay adds a NetworkPolicy that admits only
the ingress controller's namespace (set it in the overlay), and the Helm
chart renders one as soon asnetworkPolicy.fromlists a peer.
(#129) - Helm: the first upgrade rolls the Forge pods (when
secretEnvis set)
and restarts the in-cluster Redis once, because the checksum annotations
are now salted. (#133) - The published
cosign verifycommands also pin the tagged commit and the
push trigger; use the commands of the release you verify.
(#130) - Migration 0033 adds the Docker / OCI registry's tables; it changes no
existing data. To push images through a reverse proxy, allow request
bodies of a docker repository's upload limit (1 GiB by default: docker
sends a whole layer in one request) on/v2, and serve/v2at the root
of the host. The shipped Ingress now allows 1 GiB. The default
server.max_staging_bytes(four times the largest item) grows with it to
4 GiB; size the staging volume for it.
(#40) - An explicit
server.max_staging_byteskeeps working as it is. When it is
below the new 1 GiB docker defaults, they shrink to fit it (less
nuget.max_nuspec_bytes): docker repositories without an upload limit of
their own accept layers up to that size, and so do docker proxy
downloads unlessdocker.max_blob_bytesis set; startup logs a warning.
Raise the budget for larger layers. A budget too small for the package
limits is refused as before, with an error that names the setting that
decides the minimum. (#198) - The container image's baked
forge.tomlno longer holds the placeholder
minioadminS3 keys. An image started withoutFORGE_S3__ACCESS_KEY/
FORGE_S3__SECRET_KEY(and without short-lived credentials) still
refuses to start, now with an error that names the missing keys.
(#60) server.max_upload_bytesis deprecated: upload limits are now set per
hosted repository, in the web UI or the admin API. The setting still
works as the default of package repositories without a limit of their own,
and startup logs a warning while it is set; give the repositories that
need it their own limit, then remove it. A repository's limit may be at
mostserver.max_staging_bytes(lessnuget.max_nuspec_bytes); raise the
staging budget first if a repository needs more.- A workload identity realm's
max_token_age_secondsdefaults to 39600
(11 hours) instead of 3600, so GitLab jobs with a timeout over an hour
can publish. Existing realms keep the value they were saved with; to
use the new default, clear the field in the realm form (or drop the key
from the config) and save. - The username
oidcis reserved for workload identity (trusted
publishing), and 1.0.0 allowed it. An account namedoidccan no longer
sign in with its password, in the browser or over Basic, and its API
tokens work only asX-NuGet-ApiKey, no longer as the Basic password;
startup logs a warning that names it. A config with
auth.bootstrap_admin.username = "oidc"refuses to start. Rename the
account before upgrading; there is no API for it, so use SQL:
UPDATE app_user SET username = 'oidc-admin' WHERE username = 'oidc';
(the account keeps its id, password, roles and tokens). Then set
auth.bootstrap_admin.usernameto the new name if it wasoidc.
(#199) - Migration 0034 adds the
workload_api_keytable for the trusted-publishing
token exchange; it changes no existing data./api/v2/tokenis a new
route, so a reverse proxy that only forwards known prefixes needs/api.
(#169) cache.l1_max_capacitycounts an entry over 16 KiB once per started
16 KiB, so it bounds the in-process cache's memory as well as its entry
count (about 160 MiB of large entries at the default 10000). Small
entries count once, as before.
Features
-
Format plugins (preview): further package formats, served under
/repository/{repo}/(or a mount the plugin names) with the same
storage, RBAC and package selectors, hosted, proxy and group
repositories, retention, ranged downloads and audit log as NuGet. A
plugin is a crate implementing one contract (forge-plugin-api's
FormatPlugin, mirrored by theforge:format@0.3.0WIT package; a
plugin'supstreamside tells the host what to fetch for its proxies) and
runs either built into the server or as a WebAssembly component run by
Wasmtime. Components are loaded from[plugins]only when pinned by
SHA-256, and each call runs with fuel, memory and time limits and no WASI
capabilities. Helm chart repositories (plugins/helm) are built in:
index.yaml, chart downloads and ChartMuseum-compatible push, delete and
listing, and proxies of other chart repositories. The index lists chart
metadata with the types Helm reads, so a Chart.yaml withappVersion: 2
or an annotationfalsedoes not break it
(#206). A Helm proxy
parses its upstream's index once while the upstream serves the same
document, one parse at a time, instead of once per operation
(#207). Parsed indexes
are kept up to 128 MiB per instance, shared by all Helm proxies: about
60 MB ofindex.yaml, enough for every upstream of a typical group. Past
that, the indexes in use stay kept and only the others are parsed again
for each operation
(#279,
#284). NuGet is a built-in plugin too (plugins/nuget), served at
/v3/{repo}/by the same dispatcher with the same paths, responses and
proxy behaviour as before; its proxies read the upstream feed through the
plugin's upstream side.GET /admin/formatslists the installed
formats; the web UI offers them when creating a repository. See
docs/format-plugins.md. -
Docker / OCI registry under
/v2/(OCI Distribution Spec v1.1,
passing its conformance suite), for container images, Helm charts and
other OCI artifacts, as a native format next to NuGet ("format": "docker", listed byGET /admin/formats) with all three repository
kinds:- hosted: monolithic, chunked and cross-repository-mount pushes,
deletes, the referrers API for cosign signatures and attestations, and
optional immutable tags (with alatestexception); - proxy: a pull-through cache of Docker Hub (with its
library/
names), GHCR, Quay or any distribution-spec registry, with upstream
credentials, tag re-validation byHEADaftermetadata_ttl_seconds,
and stale serving while the upstream is down or rate limits; - group: members by priority, pushes to a
writable_member, and the
prefer_hosted/reserved_prefixes/ owner guards of NuGet groups.
docker logingoes through a Bearer token service (API tokens, passwords
where allowed, or a workload OIDC token), anonymous pulls follow the
repository's anonymous read, and repositories are addressed by path or by
an optional host name of their own. Image retention rules (keep the last N
tags, stale tags, untagged manifests; aHEADof a manifest counts as a
pull, #197) and a sweeper
release layers only when no image references them. The web UI lists images, tags, manifests,
platforms and referrers. New[docker]settings; see
docs/docker.md.
(#40) - hosted: monolithic, chunked and cross-repository-mount pushes,
-
Per-repository upload limits: every hosted repository (NuGet, Helm,
docker or a WebAssembly plugin format) has its own up...
v1.0.0
First production release. envisia-forge is dual-licensed under the GNU AGPL
v3 (AGPL-3.0-only, see LICENSE) or the envisia license, at the user's
choice. Install it on an empty PostgreSQL database;
docs/deployment.md
lists the required settings.
Features
- NuGet V3 registry with three repository kinds:
- hosted: push, restore, unlist/relist and delete your own packages;
- proxy: on-demand caching of an upstream V3 feed (nuget.org or a
private feed with Basic/PAT credentials), with an optional publication
delay and negative caching; - group: one feed URL over several members, by priority. A failing
member is tolerated, and a dependency-confusion guard keeps public feeds
from shadowing internal package ids.
- Storage: content-addressed blobs on any S3-compatible store; metadata in
PostgreSQL; race-free garbage collection with an outbox and a reconciler. - Access control: fine-grained RBAC with users, nested groups, roles,
repository grants, and package/content selectors, enforced on every read
and publish path. Tokens can be scoped per repository. - Authentication: ordered, web-managed auth realms (local passwords,
OIDC / Microsoft Entra, and credential forwarding to an existing registry),
an identifier-first login, and API keys. - Trusted publishing: CI jobs on GitHub Actions, GitLab CI and compatible
issuers publish with their workload OIDC token, with no stored API key. See
docs/trusted-publishing.md. - Retention: policies with preview, run and audit, and download tracking.
- Secret encryption at rest: double-envelope encryption of upstream
passwords and realm secrets, with key rotation. - Web UI: browse, repository and package detail, self-service tokens, and
administration of repositories, users, tokens and realms. It is served by
the same process on the same origin. - Operations:
/healthz,/readyzand/version, graceful SIGTERM,
supervised background workers, an audit log of every package change and
admin action (theforge::auditlog target), and an optional Redis L2
cache with cross-instance invalidation. - Container image: signed with cosign, with a provenance attestation and
an SBOM that lists the Debian packages, Rust crates and npm packages it
ships. The image carriesLICENSEand the third-party notices in
/usr/share/doc/forge/. Each release's notes print the exact
verification command. - Kubernetes manifests in
deploy/kubernetes: a kustomize base for the
server, components for an Ingress or Gateway API HTTPRoute, in-cluster
Redis, PostgreSQL and S3 (versitygw), two replicas with a
PodDisruptionBudget, and following:latest; plus a production and an
evaluation overlay. The base pins the image to the release it is tagged
with. - Helm chart in
deploy/helm/forge, published with every release to
oci://ghcr.io/schmitch/charts/forge(same version as the image, signed
with cosign, and attached to the GitHub release). The published chart pulls
the release image by the digest the release workflow signed. Forge's whole
configuration is settable asconfig(rendered toforge.toml), secrets
through a chart-managed (<fullname>-secret-env) or existing Secret;
routing through a Gateway API HTTPRoute and an optional ListenerSet (plus
a cert-manager Certificate); PodDisruptionBudget, HorizontalPodAutoscaler,
NetworkPolicy andextraObjects; and optional in-cluster PostgreSQL,
Redis and versitygw dependencies.
Security
Signing in
- Passwords are verified behind a bounded queue
(auth.password_verify_concurrency). A memory-bounded login lockout
refuses usernames over 256 bytes, keys IPv6 clients by their /64, and backs
off a username guessed from many networks, except from networks it signed
in from in the last 30 days. Unknown and federated usernames take the same
time, so response timing does not reveal which accounts exist. - Usernames are canonical (trimmed, Unicode NFC, lowercased) in every login
path, the lockout, provisioning and admin user creation. Empty names,
control characters and:are refused. - Browser sessions: a CSRF token on every cookie-authenticated mutation, and
cross-site browser mutations are refused (Sec-Fetch-Site/Origin),
which also covers Basic credentials a browser cached from the feed's login
prompt. On https the session and OIDC flow cookies carry the__Host-
prefix, and a request that carries either cookie twice is not
authenticated by it. An expired or revoked session cookie is cleared. - OIDC login uses the authorization-code flow with PKCE (S256), a nonce and
a flow bound to the browser. Only RSA signatures are accepted, andiss,
aud,expandnbfare checked. Discovery, key-set and token requests
use https and follow no redirects. A realm can require claims
(required_claims, e.g. a tenant id), an email claim becomes a username
only withemail_verified,link_existing_by_usernamenever takes over
an account another credential owns, and a realm's client secret is dropped
when itsissuerorclient_idchanges. Login starts are rate-limited per
client network (auth.oidc_login_starts_per_minute). - Credential forwarding requires https unless the upstream is a loopback
address or the realm setsallow_http. It follows no redirects, ignores
HTTP(S)_PROXY, holds no database connection during the upstream call,
and is bounded per realm (auth.credential_forward_max_concurrent) and by
the realm's timeouts. Setidentity_pathfor upstreams that accept a
secret under any username (GitHub, Azure DevOps or GitLab personal access
tokens): without it, any token holder can sign in as any user. Forge sends
the canonical, lowercased username, so an upstream that compares names
case-sensitively (an htpasswd file, for example) must know its users in
lowercase. Names that Unicode compatibility normalization (NFKC) would
change, or that hold invisible characters, are never forwarded. - The users of a deleted credential-forwarding realm are never adopted by
another realm. Upstream and IdP admin-group verdicts apply in both
directions, and a demotion ends the user's browser sessions. - Users can be deactivated (
PATCH /admin/users/{id}with
{"active": false}): their sessions and cached verdicts are revoked and
their tokens refused. A forced re-authentication
(POST /admin/users/{id}/reauth) ends their sessions and revokes the
tokens they minted from a credential-forwarding session. The last active
administrator cannot be removed, and the last enabled realm people sign in
with interactively cannot be disabled or deleted. - The bootstrap admin is created once and stays deleted after removal. Boot
logs a security warning while it still accepts the placeholder password.
Startup refuses placeholder credentials, including a secret that still
holds a<…>placeholder of the example env files, and warns about a
non-https or loopbackpublic_base_urland about disabled secret
encryption.
Tokens and the admin API
- The admin API accepts a browser session or an admin-scoped token
({"actions": ["admin"]}), never a password sent over Basic, and minting
tokens (/admin/me/tokens) takes a session. An unscoped token of an admin
can push and restore but gets 403 on/admin/*, so a leaked CI token
cannot mint tokens or create admins. An administrator's password still
signs in through/auth/login: give administrators an OIDC realm, and
NuGet clients tokens. - An expired or revoked API token, or one of a deactivated user, sent as the
Basic password is refused and never tried as a password. A token typed
into the web login's password field is refused the same way, live or not. - Admin request bodies refuse unknown fields (422 naming the field), and a
realm'sconfigrefuses unknown keys (400), as do the[oidc]and
[auth.upstream_basic]config sections that seed realms (at startup), so
a typo never falls back to an unsafe default. Token listings show each
token's scope. - Every package change (push, redeploy, unlist, relist) and every admin
change is logged at INFO under theforge::audittarget with the
principal's ids.
Reading and publishing
- RBAC applies on every feed, browse and admin path. A repository name that
does not exist answers like one the caller may not read (401 or 403), so
names cannot be enumerated. - With
anonymous_mode = "all", signed-in users can also read what
anonymous callers can; underoffandallowlistthey hold only their own
grants. Underallowlist, the anonymous grant requires aHostthat
matchespublic_base_urlorserver.allowed_hosts(a DNS-rebinding
guard), so the reverse proxy must preserve theHostheader.
X-Forwarded-Foris honoured only fromauth.trusted_proxy_cidrs, and a
malformed hop leaves the client unknown, never allowlisted. - With
allow_redeployon, replacing an existing version needs the delete
permission as well as write, so a push-only token cannot overwrite a
released version. - Groups guard against dependency confusion:
prefer_hosteddecides over
every hosted member, whatever the caller can read; ids under
reserved_prefixesare never served from a proxy member; and once a group
flags owner members, only owners claim ids. A nested group keeps its own
guards when it is read through an outer group. - Packages are read the way NuGet reads them: exactly one
.nuspecat the
package root, found with percent-decoded entry names and\as a
separator, and one<id>and<version>. Archives with repeated entry
names, Info-ZIP Unicode path fields, a<!DOCTYPE>manifest, inconsistent
ZIP64 records, or a version NuGet clients ca...