Skip to main content

Dependency proxy

The dependency proxy is a pull-through cache for public registries, run per org. You point docker, npm, pip, uv, dnf, Maven, NuGet, go, helm or cargo at your org’s proxy path on frem.sh, and fremforge fetches from the public registry on your behalf:

  • The first pull of an image layer or package file is fetched from the upstream and checked against the upstream’s own hash. If your organisation turns on malware scanning, it is also scanned with ClamAV while it streams to you. A file that fails a check is never served and never stored.
  • Later pulls are served from fremforge’s store in the EU, for every member of your org.
  • If the upstream is down, the last good copy is served. Tags, npm packuments, PyPI indexes, RPM repomd.xml, Maven maven-metadata.xml, NuGet version lists and registrations, Go version lists and @latest, Helm index.yaml files and Cargo index files are answered from the last good copy with the response header X-Fremforge-Proxy: stale. See Upstream outages.
  • Your org’s package policy and the malicious-package checks are evaluated for proxied packages and images, and every pull is recorded in your registry access log. Since 2026-10-11 a known-malicious package or image is refused for every organisation, and a policy in Enforce mode refuses what breaks it; see What is enforced today.

The proxy is read-only. You cannot push to it.

Availability and enabling it

The proxy is available on every plan and is on by default for every organisation. An organisation owner can turn it off, or change its settings, under Build & deploy › Dependency proxy or with the API (a token with the policy:write scope):

curl -X PUT -H "Authorization: Bearer $FREMFORGE_TOKEN" -H 'Content-Type: application/json' \
  -d '{"enabled": true, "malware_scan": true}' \
  https://frem.sh/_app/api/v1/orgs/<org>/dependency-proxy

An organisation that has never saved these settings has the default: the proxy on, and hosted runners pulling through it (see Hosted runners). Once an owner saves them, the organisation keeps exactly what was saved.

Malware scanning (malware_scan) is off by default. With it on, every image layer, package file and chart the proxy fetches for your organisation is scanned with ClamAV before it is served. Known-malicious packages (OSV MAL-* advisories) are matched for every organisation whatever this setting says. The same page and endpoint set block_until_scanned for container images, which needs malware scanning on (see Container images) and retention_days (7 to 365, default 90; see Retention). A GET on the endpoint shows whether the proxy is on, your usage and your cap.

Authentication

Use the same credentials as the package registry:

  • People: a personal access token with read:package (or the short-lived token fremforge auth gives your tools). You must be a member of the org.
  • CI: the Forgejo Actions job token. A job token pulls only through its own repository’s org; any other org’s proxy refuses it. Jobs from fork pull requests may pull, but never through an upstream that uses your org’s own credentials.
  • External CI, build hosts and pull secrets: a registry robot with read access. Its token is a read:package credential like any other.

Hosted runners

By default, jobs on fremforge-hosted runners install through your organisation’s proxy (runner_pull_through, on by default). At each job start the runner points npm, pip and uv at the proxy, and also Go, Maven, NuGet and the Trivy vulnerability database for the ecosystems listed in runner_pull_through_ecosystems (all four by default). The job authenticates with its own job token, so every install passes your package policy and malware checks and is in your access log with the job’s repository. If the proxy cannot be reached when the job starts, the job installs from the public registries through fremforge’s egress proxy instead, and the fallback is logged. Set runner_pull_through to false to turn this off. See also Package caches.

Container images. For the same jobs, the job’s Docker daemon also pulls container images through your organisation’s proxy: docker pull, docker run, a Dockerfile’s FROM, and the job and services: containers the runner starts. Nothing in the workflow changes; the image names stay as they are (ghcr.io/..., quay.io/...) and each pull is cached for your organisation and recorded in your access log.

  • For every organisation: ghcr.io, quay.io, mcr.microsoft.com, registry.k8s.io, public.ecr.aws and registry.access.redhat.com.
  • Only with your organisation’s own credential: docker.io, registry.redhat.io and registry.connect.redhat.com go through the proxy only once an owner has added your organisation’s credential for that registry under Upstream credentials. Jobs from fork pull requests never use it, so their pulls from these registries go to the registry directly. Without a credential, Docker Hub pulls are anonymous and subject to Docker Hub’s rate limits.
  • Fallback: the proxy is checked at the start of every job. If it cannot be reached, the job pulls every image from the registry directly, as before, and the fallback is logged. A single pull the proxy cannot serve also falls through to the registry.
  • A cache, not a gate: for images on this path, a pull that your package policy or the malware check refuses falls through to the registry too. Where a refusal must stop the pull, pull from frem.sh/<org>/dependency-proxy/<registry>/<image> explicitly (below).
  • Not covered: a docker buildx builder that uses the docker-container driver runs its own BuildKit and pulls directly. The default docker driver goes through the proxy.

Container images

docker login frem.sh            # a token with read:package, as for the registry
docker pull frem.sh/<org>/dependency-proxy/<upstream>/<image>:<tag>

For example:

docker pull frem.sh/acme/dependency-proxy/quay.io/prometheus/busybox:latest

In CI:

echo "$GITHUB_TOKEN" | docker login frem.sh -u x --password-stdin

The upstreams you can pull from:

UpstreamCredentialsStatus
docker.ioYour organisation’s own Docker Hub credential, required (below)Verified end to end
registry.k8s.io, mcr.microsoft.com, public.ecr.aws, registry.access.redhat.comNoneVerified end to end
ghcr.io, quay.ioNone for public images. Your organisation’s own credential, optional, for private images (below)Public images: verified end to end. Private images on your credential: supported, not yet verified
registry.redhat.io, registry.connect.redhat.comYour organisation’s Red Hat registry service account, requiredSupported, not yet verified: it needs a Red Hat account, which fremforge does not hold for testing

Any other host is refused: the proxy never fetches a free-form URL.

Helm charts stored as OCI artifacts on these upstreams come through the same path (Helm 3.8 or later):

echo "$FREMFORGE_TOKEN" | helm registry login frem.sh -u <user or robot name> --password-stdin
helm pull oci://frem.sh/<org>/dependency-proxy/ghcr.io/prometheus-community/charts/prometheus --version 29.36.1

For charts in a classic Helm repository (index.yaml), see Helm charts.

A tag is re-checked with the upstream at most once a minute, with a HEAD request, which Docker Hub does not count against its pull limit. Image layers pass the same pull gates as images pushed to your registry: with malware scanning on, infected or unscannable layers are refused; and so are images over your CVE threshold if you set block_pull. Malicious packages inside an image are covered below. By default an image is served while its layers are being scanned. With block_until_scanned the first pull of a not-yet-scanned image answers 503 with Retry-After until the scan has finished, usually within seconds.

Malicious packages inside cached images

The language packages inside every image your org caches (npm, PyPI, Maven, Go, crates, RubyGems, NuGet, Packagist, Pub and Hex packages) are matched against OSV’s malicious-package advisories (MAL-*), the same check that runs on the images you push:

  • after the image is first cached, usually within the next scan cycle (about 15 minutes), and
  • again every day, so an advisory published after you pulled the image is still found.

A match is a critical finding on Security › Findings › Dependencies › Malware, listed as an image named <org>/dependency-proxy/<upstream>/<repository> with the digest that carries it, and it is sent to the people who receive your security alerts. Dismissing it works as for any malware finding, with a reason and an optional end date. The finding closes by itself once your org no longer caches that image, for example after retention removes it.

A pull of an image with an open, undismissed finding is refused with 403 DENIED, as for pushed images, and logged in your access log as blocked with the reason image_malware. Refused since 2026-10-11; before that such a pull was served and recorded as would_block.

Upstream credentials

Docker Hub (docker.io) and the Red Hat registries (registry.redhat.io and registry.connect.redhat.com, Red Hat’s certified partner images) are pulled through the dependency proxy only on your organisation’s own account. For GitHub (ghcr.io) and Quay (quay.io) a credential is optional: without one they are pulled anonymously, which serves public images; add one to pull your private images. An organisation owner adds the credential under Organisation settings › Dependency proxy › Upstream credentials, or with the API at /_app/api/v1/orgs/<org>/dependency-proxy/upstreams. Accepted forms:

  • Docker Hub: your username plus a personal access token, or an organisation access token with the Docker organisation name as the username.
  • Red Hat: a registry service account (a username such as 1234567|ci, plus its token), or its OpenShift pull secret. Add it once per Red Hat registry you pull from; each keeps only its own entry of the pull secret.
  • GitHub (ghcr.io): your GitHub username plus a personal access token (classic) with read:packages.
  • Quay (quay.io): a robot account (organisation+robot) plus its token, or the quay.io entry of a Red Hat pull secret.

The Red Hat registries with your credential, and private ghcr.io and quay.io images, are supported but not yet verified end to end: fremforge has tested them against the registries’ token flow, but holds no account to pull with.

Once a ghcr.io or quay.io credential is stored, every pull from that registry uses it, public images included, and is cached for your organisation only. Jobs from fork pull requests never use it: they pull those two registries anonymously, so they get public images only.

The secret is stored encrypted and used only by the dependency proxy, for your own organisation’s pulls. It is never shown again; you see a fingerprint instead. Test connection checks it and, for Docker Hub, shows how many pulls are left in your current limit window.

Images pulled with your credential are cached for your organisation only, and never served to another organisation. Jobs from fork pull requests cannot pull through a registry that requires your credential. If the credential expires, images already cached keep being served and the status shows Refused by the registry. Without a credential, a Docker Hub pull is refused with a message that says so.

Kubernetes and OpenShift

Point a mirror at the proxy. containerd, in /etc/containerd/certs.d/quay.io/hosts.toml:

server = "https://quay.io"

[host."https://frem.sh/v2/<org>/dependency-proxy/quay.io"]
  capabilities = ["pull", "resolve"]
  override_path = true

OpenShift, with an ImageTagMirrorSet (and an ImageDigestMirrorSet for digests):

apiVersion: config.openshift.io/v1
kind: ImageTagMirrorSet
metadata:
  name: frem-sh-dependency-proxy
spec:
  imageTagMirrors:
    - source: quay.io
      mirrors:
        - frem.sh/<org>/dependency-proxy/quay.io

The cluster’s pull secret needs a frem.sh token with read:package.

npm

.npmrc in your project or home directory:

registry=https://frem.sh/_app/proxy/<org>/npm/
//frem.sh/_app/proxy/<org>/npm/:_authToken=${FREMFORGE_TOKEN}

Then use npm, pnpm or yarn as usual. Packages come from registry.npmjs.org, scoped packages included. Each tarball must match the integrity (sha512) the npm registry publishes for it before it is served.

npm audit and pnpm audit work against the proxy. They are answered from fremforge’s own copy of the OSV advisory database, so your dependency list is not sent to any third-party advisory service. Supported clients and the response format are described under npm audit and pnpm audit through the dependency proxy.

To combine the proxy with packages your org publishes in its own registry, route your scope to the registry and everything else to the proxy:

registry=https://frem.sh/_app/proxy/<org>/npm/
@<your-scope>:registry=https://frem.sh/api/packages/<org>/npm/
//frem.sh/_app/proxy/<org>/npm/:_authToken=${FREMFORGE_TOKEN}
//frem.sh/api/packages/<org>/npm/:_authToken=${FREMFORGE_TOKEN}

PyPI

pip install --index-url "https://<user>:${FREMFORGE_TOKEN}@frem.sh/_app/proxy/<org>/pypi/simple/" requests
uv pip install --index-url "https://<user>:${FREMFORGE_TOKEN}@frem.sh/_app/proxy/<org>/pypi/simple/" requests

Or in pip.conf / uv.toml, with the credential in a netrc file or keyring rather than the URL. The index is served in both formats installers use (PEP 503 HTML and PEP 691 JSON), with each file’s sha256, requires-python, yank status and PEP 658 metadata. Each file must match the sha256 that PyPI lists for it before it is served.

Use the proxy as your only index. Do not add it with --extra-index-url next to another index: mixing indexes is what makes dependency-confusion attacks possible.

RPM (dnf and yum)

Each repository is reached at

https://frem.sh/_app/proxy/<org>/rpm/<upstream>/<repository path>

Today the upstream is cdn-ubi.redhat.com, Red Hat’s Universal Base Image repositories (UBI 8, 9 and 10: BaseOS, AppStream and CodeReady Builder, for every architecture Red Hat publishes), under content/public/ubi/. They need no Red Hat account. A .repo file for UBI 9 on x86_64:

[ubi-9-baseos-fremforge]
name=UBI 9 BaseOS through fremforge
baseurl=https://frem.sh/_app/proxy/<org>/rpm/cdn-ubi.redhat.com/content/public/ubi/dist/ubi9/9/x86_64/baseos/os
username=<user or robot name>
password=<token with read:package>
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

[ubi-9-appstream-fremforge]
name=UBI 9 AppStream through fremforge
baseurl=https://frem.sh/_app/proxy/<org>/rpm/cdn-ubi.redhat.com/content/public/ubi/dist/ubi9/9/x86_64/appstream/os
username=<user or robot name>
password=<token with read:package>
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

dnf sends username and password as HTTP Basic authentication. Use a registry robot for build hosts and image builds, and keep the file readable only by root (chmod 600), because it holds the token. In a container build, disable the image’s own repositories for the install (dnf --disablerepo='*' --enablerepo='*-fremforge' install …), so nothing is fetched around the proxy.

What the proxy does:

  • repodata/repomd.xml is re-checked with the upstream at most every 5 minutes and served exactly as the upstream published it. A new repomd.xml is only accepted once the primary.xml it names has been downloaded and matches the sha256 and size repomd.xml gives for it. Until then, and while the upstream is down, the previous one is served.
  • Repository metadata files (primary, filelists, other, updateinfo, modules, comps and the sqlite variants) must match the sha256 and size in repomd.xml.
  • Packages must match the sha256 and size in primary.xml. With malware scanning on, each package is also scanned inside its compressed payload, not only as a file, and its install scripts are scanned too. A package the scanner cannot open is then refused, not served unscanned.
  • Package signatures are checked by dnf itself (gpgcheck=1), against Red Hat’s key, exactly as without the proxy. The proxy never changes a package or the repository metadata.
  • RPM content is cached for your organisation only. Another organisation pulling the same package fetches and stores its own copy. This is so that the same code path can later serve Red Hat content fetched on your own Red Hat subscription, which must never reach another customer.

A repository that checksums with anything but sha256 is not served. Proxying cdn.redhat.com with your Red Hat entitlement certificate is planned and not yet available.

Maven

Maven Central is reached at

https://frem.sh/_app/proxy/<org>/maven/central/

Make it the mirror for every repository in ~/.m2/settings.xml (or the settings.xml your build uses), with the credential in a <server> of the same id:

<settings>
  <servers>
    <server>
      <id>fremforge-proxy</id>
      <username><user or robot name></username>
      <password>${env.FREMFORGE_TOKEN}</password>
    </server>
  </servers>
  <mirrors>
    <mirror>
      <id>fremforge-proxy</id>
      <name>Maven Central through fremforge</name>
      <url>https://frem.sh/_app/proxy/<org>/maven/central/</url>
      <mirrorOf>*</mirrorOf>
    </mirror>
  </mirrors>
</settings>

Maven sends the username and password as HTTP Basic authentication when the proxy asks for them. To use the proxy together with packages your org publishes in its own registry, write <mirrorOf>*,!fremforge</mirrorOf> and declare your registry (https://frem.sh/api/packages/<org>/maven) as repository fremforge with its own <server>.

Gradle (settings.gradle.kts):

dependencyResolutionManagement {
    repositories {
        maven {
            url = uri("https://frem.sh/_app/proxy/<org>/maven/central/")
            credentials {
                username = providers.environmentVariable("FREMFORGE_USER").get()
                password = providers.environmentVariable("FREMFORGE_TOKEN").get()
            }
        }
    }
}

What the proxy does:

  • maven-metadata.xml is re-checked with Maven Central at most every 5 minutes. A new copy is accepted only when it matches Maven Central’s own .sha1 of it. Until then, and while Maven Central is down, the previous copy is served. Its .sha1, .md5, .sha256 and .sha512 are computed from the copy you are served, so they always match it.
  • Artifacts (jars, POMs, .module files and every other file of a release) must match Maven Central’s .sha1 and, when Maven Central publishes one, its .sha256. A file with neither checksum is not fetched. With malware scanning on, each file is scanned as it streams to you. A jar is also opened: an entry the scanner cannot unpack itself (zstd) is unpacked for it, and a jar with an entry neither can open (for example LZMA) is refused rather than served unscanned.
  • Checksum files (.sha1, .sha256, …) and signatures (.asc) of artifacts are passed through unchanged. Check signatures with your build’s PGP plugin, as you would without the proxy.
  • Snapshots are not served. Maven Central publishes none, and a snapshot can change under the same version. A -SNAPSHOT request is answered 404, so your build moves on to the repository that has it.

For the package policy, a Maven package is named groupId:artifactId. Its release time is when Maven Central published the file, and its licence is the <licenses> its POM declares. A licence inherited from a parent POM is not looked up, so it counts as not assessed.

NuGet

The package source is the proxy’s service index:

https://frem.sh/_app/proxy/<org>/nuget/v3/index.json

nuget.config next to your solution:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="fremforge" value="https://frem.sh/_app/proxy/<org>/nuget/v3/index.json" />
  </packageSources>
  <packageSourceCredentials>
    <fremforge>
      <add key="Username" value="<user or robot name>" />
      <add key="ClearTextPassword" value="%FREMFORGE_TOKEN%" />
    </fremforge>
  </packageSourceCredentials>
</configuration>

<clear /> makes the proxy the only source, so no package is fetched around it. NuGet expands %FREMFORGE_TOKEN% from the environment, so the token does not have to be in the file. Then use dotnet restore, dotnet add package <id> --version <version> or nuget install as usual.

What the proxy does:

  • The service index lists only resources on frem.sh: the package content (flat container) and the package registration. Search is not provided, because restore and install do not need it. dotnet add package without --version needs search, so give the version.
  • Version lists and registration are re-checked with nuget.org at most every 5 minutes, and the last good copy is served while nuget.org is down.
  • Packages (.nupkg) must match the SHA-512 and size that nuget.org’s catalog records for the release before they are served. A release whose catalog entry carries no hash is not fetched. With malware scanning on, each package is scanned as it streams to you, inside the package as well as as a file.
  • Package signatures are checked by the NuGet client itself, exactly as without the proxy. The proxy never changes a package.

For the package policy, a NuGet package is named by its id in lower case. Its release time and licence are nuget.org’s published date and the package’s declared licence expression. A package that declares its licence as a file or a URL counts as not assessed.

Go modules

export GOPROXY=https://frem.sh/_app/proxy/<org>/go
export GOSUMDB=sum.golang.org      # the default; keep it on

Authentication comes from ~/.netrc (_netrc on Windows), which the go command reads for frem.sh. Keep the file readable only by you (chmod 600 ~/.netrc):

machine frem.sh
login <user or robot name>
password <token with read:package>

Use GOPROXY=https://frem.sh/_app/proxy/<org>/go rather than adding ,direct: with direct, a module the proxy does not serve is fetched from its source repository around the proxy.

What the proxy does:

  • @v/list and @latest are re-checked with proxy.golang.org at most every 5 minutes, and the last good copy is served while it is down.
  • .mod and .zip must match the h1: hashes that the Go checksum database (sum.golang.org) has signed for that module version. The proxy verifies the database’s signature with the same key the go command has built in. A module zip is unpacked and hashed the way the go command hashes it before it is served. A module the checksum database does not know is not served. With malware scanning on, each zip is scanned as it streams to you.
  • The checksum database is reached through the proxy too. The go command asks the proxy whether it serves sum.golang.org, and then fetches every lookup and tile through it. So GOSUMDB stays on and still does its own full verification, including the proofs that the proxy itself does not repeat, and your machine needs no direct access to sum.golang.org.
  • Private modules are not served: proxy.golang.org cannot fetch them, and the checksum database does not know them. Exclude them with GOPRIVATE (or GONOPROXY and GONOSUMDB), and the go command fetches them from your own Git hosting as before.

For the package policy, a Go module is named by its module path. Its release time is the time proxy.golang.org records for the version. Go modules declare no licence the proxy can read, so licence rules do not apply to them.

Helm charts

Classic Helm repositories (an index.yaml and chart archives) are served from a fixed list:

Repository nameUpstream
prometheus-communityhttps://prometheus-community.github.io/helm-charts
grafanahttps://grafana.github.io/helm-charts
jetstackhttps://charts.jetstack.io (cert-manager)
ingress-nginxhttps://kubernetes.github.io/ingress-nginx

Add a repository with the proxy’s URL and a credential with read:package:

helm repo add prometheus-community https://frem.sh/_app/proxy/<org>/helm/prometheus-community \
  --username <user or robot name> --password-stdin <<< "$FREMFORGE_TOKEN"
helm search repo prometheus-community/prometheus
helm pull prometheus-community/prometheus --version 29.36.1
helm install my-prometheus prometheus-community/prometheus --version 29.36.1

Every chart URL in the index points back to frem.sh, so Helm sends your credential with each chart download and --pass-credentials is not needed.

What the proxy does:

  • index.yaml is re-checked with the repository at most every 5 minutes, and the last good copy is served while the repository is down or answers something that is not a Helm index. The proxy returns it as JSON, which Helm reads as YAML.
  • Charts must match the digest (sha256) that the repository’s index records for that version before they are served. A version whose index entry has no digest, or whose chart is not on the repository’s own hosts (for example an oci:// URL), is left out of the index you get. With malware scanning on, each chart is scanned as it streams to you.
  • Provenance files (.prov) are passed through unchanged, so helm pull --verify and helm install --verify work with the chart publisher’s public key, for example for cert-manager: helm pull jetstack/cert-manager --version v1.21.2 --verify --keyring cert-manager-keyring.gpg. A provenance file must name the chart with the index’s digest, or it is not served. Of the four repositories, jetstack and prometheus-community publish provenance files; for a chart without one, --verify fails with failed to fetch provenance (404).
  • A chart’s dependencies are resolved by Helm from the repositories its Chart.yaml names. A helm dependency update reaches those repositories directly unless you add them through the proxy as well.

Bitnami’s classic repository is not on the list: its current chart versions are OCI artifacts on Docker Hub. Pull them through the container path, oci://frem.sh/<org>/dependency-proxy/docker.io/bitnamicharts/<chart>, which uses your organisation’s own Docker Hub credential (above).

For the package policy, a chart is named by its chart name. Its release time is the created date in the index, and its licence the artifacthub.io/license annotation, if the chart has one.

Cargo (crates.io)

The proxy is a sparse registry for crates.io. It needs Cargo 1.74 or later. Replace crates-io with it in .cargo/config.toml, in your project or in ~/.cargo/config.toml for every project:

[source.crates-io]
replace-with = "fremforge"

[registries.fremforge]
index = "sparse+https://frem.sh/_app/proxy/<org>/cargo/index/"
credential-provider = "cargo:token"

Cargo sends the token as the whole Authorization header, so give it the Bearer prefix. Keep it out of the file, in the environment:

export CARGO_REGISTRIES_FREMFORGE_TOKEN="Bearer $FREMFORGE_TOKEN"
cargo fetch
cargo build --locked

With the source replacement, every crate from crates.io comes through the proxy, including the crates an existing Cargo.lock pins, and no crate is fetched around it.

What the proxy does:

  • config.json is the proxy’s own. It points crate downloads back to your org’s path on frem.sh and tells Cargo that the registry needs authentication, so Cargo sends the token with every index and download request.
  • Index files come from index.crates.io. They are re-checked at most every 5 minutes, and the last good copy is served while crates.io is down. Each file is crates.io’s own, version for version. Yanked versions stay in it, marked yanked: Cargo still uses one that your Cargo.lock pins, and does not pick one for a new resolution.
  • Crates (.crate) come from static.crates.io and must match the cksum (sha256) in their index entry before they are served. A version whose index entry has no checksum is left out. With malware scanning on, each crate is scanned as it streams to you, inside the archive as well. Cargo checks the same checksum again on its side.
  • Search, publishing, yanking and owners are not provided: cargo search answers 404 and cargo publish is refused with 405. Publish your own crates to your org’s Cargo registry.

For the package policy, a crate is named by its name as crates.io publishes it, and its release time is the pubtime in its index entry. The index carries no licence, so licence rules do not apply to crates. Vulnerabilities and malicious crates come from OSV’s crates.io data: the RustSec advisories and the MAL-* records.

Package policy and malicious packages

Your org’s package policy is evaluated for npm, PyPI, RPM, Maven, NuGet, Go and Cargo packages and Helm charts fetched through the proxy, with the same rules as for packages in your registry:

  • Licence allow, deny and warn lists, applied to the version’s declared licence.
  • Vulnerabilities at or above your severity or CVSS threshold, from the OSV database.
  • Deny and allow lists by package name and version range.
  • Minimum release age, from the upstream’s publish time.
  • Malicious packages (OSV MAL-* advisories) are always checked, whatever the policy says.

For RPM packages, the version is [epoch:]version-release, the licence is the package’s License field, and the release age is when the package entered the repository. OSV has no malicious-package or vulnerability data for RPM packages, so those two checks do not apply to RPMs in the proxy. Vulnerabilities in RPM packages are found when the images built from them are scanned. The same holds for Helm charts: OSV has no data for them, so licence, deny lists and release age apply, and the images a chart deploys are scanned where they are pulled. Once refusing is switched on, a blocked chart version stays in the Helm index and its download is refused with 403.

Refusing is on since 2026-10-11. A policy in enforce mode removes a blocked version from the npm packument or PyPI index, so your resolver picks an allowed version, and a direct download of it is refused with 403 naming the rule. A policy in shadow mode, the default, still serves the package and records the pull in the access log as would_block with the rule that matched. Malicious packages are refused whatever your policy’s mode, Shadow and Off included, and left out of the package metadata the proxy serves.

While the proxy cannot get a policy answer from fremforge (after a few seconds of retries, and without a recent answer for the same package to fall back on), it refuses the download with 503 rather than serve a version it could not check for malware. Retrying later succeeds once the answer is back.

Advisory data is refreshed from fremforge’s OSV mirror several times a day, and a policy change applies to the next pull. Nothing needs to be rescanned.

What is enforced today

CheckApplies toToday
Upstream checksum (image digest, npm integrity, PyPI sha256, RPM repomd.xml and primary.xml sha256 and size, Maven .sha1 and .sha256, NuGet catalog SHA-512 and size, Go h1: signed by the checksum database, Helm index.yaml digest, Cargo index cksum)Every layer, package and repository metadata fileRefused on mismatch; nothing is stored
Antivirus (ClamAV) while the file streams to you, if your organisation turned on malware scanning (malware_scan, off by default)Every layer, package file and chart; RPM payloads unpacked; jar, nupkg and module-zip entries the scanner cannot open itself unpacked, or refusedRefused: an infected file is cut off before its last byte and never stored
Layer scan result, infected or unscannable (with malware scanning on)Container imagesRefused, as for images you push
Vulnerability block_pull, if your organisation set itContainer imagesRefused with 403 DENIED; see Image scanning
Malicious packages (OSV MAL-*) inside the imageContainer imagesRefused, as for images you push; a finding on the Malware tab
Malicious packages (OSV MAL-*)npm, PyPI, Maven, NuGet, Go, CargoRefused with 403, whatever your policy’s mode
Package policy: licence, vulnerabilities, deny list, release agenpm, PyPI, Maven, NuGet, Go and Cargo (no licence for Go and Cargo)Refused when your policy is in enforce mode; recorded (would_block) in shadow mode
Package policy: licence, deny list, release ageRPM, Helm chartsRefused when your policy is in enforce mode; recorded (would_block) in shadow mode

Cached images are also scanned for vulnerabilities and leaked secrets like the images you push; see Image scanning.

Storage, caps and billing

Cached files count toward your organisation’s package storage at their full size, as if they were in your own registry. A container layer or npm or PyPI file that several organisations pull is stored once, but each organisation’s usage counts it. RPM content is stored per organisation (see RPM).

  • Paid plans: cached files share one package storage pool with your own packages. With overage off, the proxy may use what your own packages leave of the pool. When nothing is left, new fetches are refused with 403 (dependency proxy storage cap reached), and what is already cached is still served. With overage on there is no proxy cap, and storage above the pool is billed at the package overage rate.
  • Trials: a flat 5 GB of cached files.

The Dependency proxy page shows your usage and your cap. The figure for your own packages that the cap is worked out from is refreshed once a day.

Retention

A cached file your organisation has not pulled for retention_days (default 90, from 7 to 365) is removed from your cache, together with the tags that point to it, and stops counting toward your storage. The check runs every hour. Once no organisation references a file, it is deleted from fremforge’s store 24 hours later. Pulling it again fetches it from the upstream.

Package versions still in use are kept. A package file (npm, PyPI, Maven, NuGet, Go, Cargo, Helm or RPM) is not removed, however long ago it was last pulled, while the same package version is a current component in your organisation’s component inventory: in the current SBOM of a repository’s default branch, its latest release, a current image, a live registry package or an uploaded SBOM. A dependency pinned in a lockfile but built only twice a year therefore stays cached. It still counts toward your storage. Once the version leaves every current SBOM, the normal rule applies again at the next hourly check.

Base images your current builds use are kept. A cached container image is not removed, however long ago it was last pulled, while one of your organisation’s current images is built on it: every layer of the cached image is also a layer of an image that is still published under a tag in your organisation’s registry. The image’s manifest, its configuration and all its layers are kept together, so a pull of it keeps working, and so is a multi-platform index whose platform image you build on. An image that only shares a layer with your builds (for example another image on the same distribution) is not kept by this rule. It still counts toward your storage. Once no current image is built on it, the normal rule applies again. Every other cached image keeps the last-pulled rule. Two cases are not recognised and keep the last-pulled rule: a base used only for a platform other than linux/amd64, and a build that recompresses the base image’s layers instead of reusing them.

Upstream outages

When an upstream registry is down, slow or refusing, builds keep working for everything your organisation has pulled before:

  • Files you have pulled before (image layers, .tgz, wheels, .jar, .nupkg, Go .zip and .mod, Helm charts, crates) are served from fremforge’s store. Serving them never waits on the upstream.
  • Indexes and tags (image tags, npm packuments, PyPI indexes, RPM repomd.xml, Maven maven-metadata.xml, NuGet version lists and registrations, Go @v/list and @latest, Helm index.yaml, Cargo index files) are answered from the last good copy. The response then carries X-Fremforge-Proxy: stale, so a new release published during the outage is not visible until the upstream is back.
  • Image tags another organisation has cached are served from the last good copy too, when your organisation has never pulled that tag: only for public images pulled without credentials, and only from fremforge’s shared store, never from content another organisation fetched with its own registry credentials. Docker Hub images are always fetched with your organisation’s own credential and are not shared this way.
  • Anything never pulled before answers 502, because there is nothing to serve.

If fremforge’s own sign-in or authorisation service is briefly unavailable, requests that were allowed in the last 15 minutes keep being served from the cache (reads only). A revoked token or a removed membership still takes effect as soon as fremforge can check it.

What survives an outage is bounded by retention. A file your organisation has not pulled for retention_days (default 90) is removed. After that it has to be fetched from the upstream again, which is not possible while the upstream is down. Package versions in your component inventory, and the base images your current images are built on, are kept regardless (see Retention). For anything else you need to survive a long outage, pull it at least once within your retention period, or raise retention_days.

Access log

Every pull through the proxy appears in your org’s registry access log with origin proxy, except a CI job’s allowed metadata reads (package indexes and version lists), which are not stored since 2026-10-12; its downloads, refusals and errors are. The log records the package, version and file, who pulled it (a user, or for CI the job’s repository), the outcome, and any policy decision. A pull served from the cache is a redirect to object storage (307), and its size is recorded too. A HEAD request is recorded as GET, because the CDN forwards HEAD requests to fremforge as GET.

What the upstream sees

fremforge contacts the upstream you chose, on your behalf. The upstream receives fremforge’s IP address and the name of the image or package. For docker.io, the Red Hat registries, and ghcr.io or quay.io when you have stored a credential for them, it also receives your organisation’s own credential, so the pull counts against your account there. It receives nothing about your users. Most of these registries are run by companies outside the EU. After the first fetch, pulls are served from fremforge’s EU store without contacting the upstream again, except to re-check tags, npm and PyPI metadata, RPM repomd.xml, Maven maven-metadata.xml and NuGet version lists and registration, Go version lists, Helm index.yaml files and Cargo index files (every 5 minutes at most). Go’s checksum database is reached through the proxy as well, on the go command’s behalf.

Errors

StatusMeaning
401No credential, or frem.sh did not accept it.
403Not a member, the proxy is turned off for the org, the credential lacks read:package, a job token for another org, your org’s storage cap is reached, or a refusal by a check (the message names it).
404The upstream does not have it, or the upstream host is not available through the proxy.
405A write. The proxy is read-only.
429Your org’s upstream fetch limit was reached. Retry after the Retry-After seconds.
502The upstream is unavailable and nothing is cached for this request.
503A dependency (malware scanner, policy check) is unavailable. Retry after Retry-After.