Skip to content

Releases: forgepkg/forge

v2.1.2

Choose a tag to compare

@github-actions github-actions released this 29 Oct 22:42
Immutable release. Only release title and notes can be modified.
b2d961b

Fixed

  • A first pull of a large layer through a Docker proxy could fail with
    internal error while the layer was stored all the same, and the next pull
    served it: the server logged registry: blob read with NoSuchKey. When the
    answer to an upload's CompleteMultipartUpload (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

Choose a tag to compare

@github-actions github-actions released this 29 Oct 22:15
Immutable release. Only release title and notes can be modified.
5e06a1a

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 with Retry-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's MALLOC_* variables no longer apply.
    (#669)
  • Forge can publish patch releases of an older line, from its maintenance
    branch, while main heads 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 :latest the 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 of typescript, @types/node, react-dom, tailwindcss and
    vite at 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 than proxy.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

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

Choose a tag to compare

@github-actions github-actions released this 28 Oct 08:39
Immutable release. Only release title and notes can be modified.
7f5f273

Upgrade notes

  • The anonymous mode allowlist and its anonymous_ip_allowlist are removed;
    the anonymous modes are off and all. An instance whose stored mode is
    allowlist comes up with anonymous access off, and the settings API
    refuses "anonymous_mode": "allowlist" and anonymous_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
    anonymous role 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=Recreate or scale the deployment to zero
    first; with the Kubernetes manifests, scale the forge deployment 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 standard OTEL_* 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, and telemetry.per_repository = false
    takes the repository names off. A request is a trace, joining the caller's
    from its traceparent, 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 component components/opentelemetry turn it on, and
    deploy/monitoring has an example Collector configuration, a Grafana
    dashboard and alerting rules. See the deployment guide's Telemetry section.
    (#178)
  • Trusted-header realms: a trusted_header realm (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 with auto_provision) and never makes anyone an administrator: its
    accounts never hold the admin role. 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, or POST /admin/users with
    "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 the admin
    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/users gains kind,
    description, realm and last_used_at in 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 as DELETE /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 an older_than_days,
    not_downloaded_for_days or not_pulled_for_days boundary 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_bytes of 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_total and manifests_total in
    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's reserved_prefixes, or of a name an owning
    hosted member has (prefer_hosted), was listed with a docker 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 as evaluated ("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 (type 7) included: a package.json of that type after a regular
    one is the version's manifest, as it is the one npm installs, and such
    entries count in dist.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...
Read more

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 20:10
Immutable release. Only release title and notes can be modified.
17a612c

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 oidc realm 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-configuration and 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: extraObjects can 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 and npm deprecate with the level write, and npm unpublish
    with delete. Proxies cache registry.npmjs.org or any registry, with
    Basic credentials or a token sent as Authorization: Bearer to 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 pass npm audit on to their proxy
    without the reserved names. npm login is approved in the browser on a
    page of the web UI, or checks a password (yarn npm login), and mints an
    API token that npm logout revokes; 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 .npmrc and .yarnrc.yml lines 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_redeploy says 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 a PUT with an
    API token or a multipart form (Nexus' raw.directory / raw.assetN fields
    work); downloads answer one Range, conditional requests, an ETag (the
    SHA-256) and Last-Modified, and <file>.sha256, .sha512, .sha1 and
    .md5 from the stored digests, so sha256sum -c checks 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 them inline, 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 after max_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
    curl commands. 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, the helm install and helm pull
    commands of the selected one (the newest release by default) with the
    helm registry login to run first, its Chart.yaml facts (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_n keeps the newest N cached versions the upstream
    lists, and keep_n_most_downloaded the 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/forge for
    linux/amd64 and linux/arm64 as 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 verify command passes for each of their
    digests, and the Helm chart pins the index digest. cargo xtask docker-build --platform linux/arm64 builds 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...
Read more

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 22 Oct 16:20
Immutable release. Only release title and notes can be modified.
cc1fbfd

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_dir that 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 new docker.upload_ttl_seconds,
    docker.unreferenced_blob_ttl_seconds and docker.sweep_interval_seconds)
    fail validation instead of panicking later.
    (#128)
  • Migration 0032 deletes grants of the admin action 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 as networkPolicy.from lists a peer.
    (#129)
  • Helm: the first upgrade rolls the Forge pods (when secretEnv is set)
    and restarts the in-cluster Redis once, because the checksum annotations
    are now salted. (#133)
  • The published cosign verify commands 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 /v2 at 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_bytes keeps 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 unless docker.max_blob_bytes is 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.toml no longer holds the placeholder
    minioadmin S3 keys. An image started without FORGE_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_bytes is 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
    most server.max_staging_bytes (less nuget.max_nuspec_bytes); raise the
    staging budget first if a repository needs more.
  • A workload identity realm's max_token_age_seconds defaults 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 oidc is reserved for workload identity (trusted
    publishing), and 1.0.0 allowed it. An account named oidc can no longer
    sign in with its password, in the browser or over Basic, and its API
    tokens work only as X-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.username to the new name if it was oidc.
    (#199)
  • Migration 0034 adds the workload_api_key table for the trusted-publishing
    token exchange; it changes no existing data. /api/v2/token is a new
    route, so a reverse proxy that only forwards known prefixes needs /api.
    (#169)
  • cache.l1_max_capacity counts 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 the forge:format@0.3.0 WIT package; a
    plugin's upstream side 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 with appVersion: 2
    or an annotation false does 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 of index.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/formats lists 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 by GET /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 a latest exception);
    • 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 by HEAD after metadata_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 login goes 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; a HEAD of 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)

  • Per-repository upload limits: every hosted repository (NuGet, Helm,
    docker or a WebAssembly plugin format) has its own up...

Read more

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 12:17
Immutable release. Only release title and notes can be modified.
2be2082

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, /readyz and /version, graceful SIGTERM,
    supervised background workers, an audit log of every package change and
    admin action (the forge::audit log 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 carries LICENSE and 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 as config (rendered to forge.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 and extraObjects; 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, and iss,
    aud, exp and nbf are 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 with email_verified, link_existing_by_username never takes over
    an account another credential owns, and a realm's client secret is dropped
    when its issuer or client_id changes. 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 sets allow_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. Set identity_path for 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 loopback public_base_url and 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's config refuses 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 the forge::audit target 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; under off and allowlist they hold only their own
    grants. Under allowlist, the anonymous grant requires a Host that
    matches public_base_url or server.allowed_hosts (a DNS-rebinding
    guard), so the reverse proxy must preserve the Host header.
    X-Forwarded-For is honoured only from auth.trusted_proxy_cidrs, and a
    malformed hop leaves the client unknown, never allowlisted.
  • With allow_redeploy on, 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_hosted decides over
    every hosted member, whatever the caller can read; ids under
    reserved_prefixes are 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 .nuspec at 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...
Read more