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, Mavenmaven-metadata.xml, NuGet version lists and registrations, Go version lists and@latest, Helmindex.yamlfiles and Cargo index files are answered from the last good copy with the response headerX-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-proxyAn 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 tokenfremforge authgives 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:packagecredential 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.awsandregistry.access.redhat.com. - Only with your organisation’s own credential:
docker.io,registry.redhat.ioandregistry.connect.redhat.comgo 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 buildxbuilder that uses thedocker-containerdriver runs its own BuildKit and pulls directly. The defaultdockerdriver 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:latestIn CI:
echo "$GITHUB_TOKEN" | docker login frem.sh -u x --password-stdinThe upstreams you can pull from:
| Upstream | Credentials | Status |
|---|---|---|
docker.io | Your organisation’s own Docker Hub credential, required (below) | Verified end to end |
registry.k8s.io, mcr.microsoft.com, public.ecr.aws, registry.access.redhat.com | None | Verified end to end |
ghcr.io, quay.io | None 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.com | Your organisation’s Red Hat registry service account, required | Supported, 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.1For 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) withread:packages. - Quay (
quay.io): a robot account (organisation+robot) plus its token, or thequay.ioentry 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 = trueOpenShift, 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.ioThe 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/" requestsOr 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-releasednf 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.xmlis re-checked with the upstream at most every 5 minutes and served exactly as the upstream published it. A newrepomd.xmlis only accepted once theprimary.xmlit names has been downloaded and matches the sha256 and sizerepomd.xmlgives for it. Until then, and while the upstream is down, the previous one is served.- Repository metadata files (
primary,filelists,other,updateinfo,modules,compsand the sqlite variants) must match the sha256 and size inrepomd.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.xmlis re-checked with Maven Central at most every 5 minutes. A new copy is accepted only when it matches Maven Central’s own.sha1of it. Until then, and while Maven Central is down, the previous copy is served. Its.sha1,.md5,.sha256and.sha512are computed from the copy you are served, so they always match it.- Artifacts (jars, POMs,
.modulefiles and every other file of a release) must match Maven Central’s.sha1and, 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
-SNAPSHOTrequest is answered404, 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.jsonnuget.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 packagewithout--versionneeds 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 onAuthentication 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/listand@latestare re-checked withproxy.golang.orgat most every 5 minutes, and the last good copy is served while it is down..modand.zipmust match theh1: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. SoGOSUMDBstays 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 tosum.golang.org. - Private modules are not served:
proxy.golang.orgcannot fetch them, and the checksum database does not know them. Exclude them withGOPRIVATE(orGONOPROXYandGONOSUMDB), 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 name | Upstream |
|---|---|
prometheus-community | https://prometheus-community.github.io/helm-charts |
grafana | https://grafana.github.io/helm-charts |
jetstack | https://charts.jetstack.io (cert-manager) |
ingress-nginx | https://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.1Every 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.yamlis 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 anoci://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, sohelm pull --verifyandhelm install --verifywork 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,--verifyfails withfailed to fetch provenance(404). - A chart’s dependencies are resolved by Helm from the repositories its
Chart.yamlnames. Ahelm dependency updatereaches 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 --lockedWith 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.jsonis the proxy’s own. It points crate downloads back to your org’s path onfrem.shand 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 yourCargo.lockpins, and does not pick one for a new resolution. - Crates (
.crate) come fromstatic.crates.ioand must match thecksum(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 searchanswers404andcargo publishis refused with405. 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
| Check | Applies to | Today |
|---|---|---|
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 file | Refused 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 refused | Refused: an infected file is cut off before its last byte and never stored |
| Layer scan result, infected or unscannable (with malware scanning on) | Container images | Refused, as for images you push |
Vulnerability block_pull, if your organisation set it | Container images | Refused with 403 DENIED; see Image scanning |
Malicious packages (OSV MAL-*) inside the image | Container images | Refused, as for images you push; a finding on the Malware tab |
Malicious packages (OSV MAL-*) | npm, PyPI, Maven, NuGet, Go, Cargo | Refused with 403, whatever your policy’s mode |
| Package policy: licence, vulnerabilities, deny list, release age | npm, 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 age | RPM, Helm charts | Refused 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.zipand.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, Mavenmaven-metadata.xml, NuGet version lists and registrations, Go@v/listand@latest, Helmindex.yaml, Cargo index files) are answered from the last good copy. The response then carriesX-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
| Status | Meaning |
|---|---|
401 | No credential, or frem.sh did not accept it. |
403 | Not 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). |
404 | The upstream does not have it, or the upstream host is not available through the proxy. |
405 | A write. The proxy is read-only. |
429 | Your org’s upstream fetch limit was reached. Retry after the Retry-After seconds. |
502 | The upstream is unavailable and nothing is cached for this request. |
503 | A dependency (malware scanner, policy check) is unavailable. Retry after Retry-After. |