Package formats¶
Repod hosts seven package formats behind one backend, one RBAC model, one audit
trail, and one security pipeline. This page explains how the different formats
relate to each other and to the OS-package (REPO_FORMAT) toggle described in
Architecture.
Two independent groups¶
OS packages — .deb, .rpm, .apk — are selected by the REPO_FORMAT
environment variable at startup. An instance serves exactly the OS-package
formats you configure it for, using the native repository tooling for each
(reprepro, createrepo_c, apk index).
Build-tool packages — Maven, PyPI, npm — and the OCI container registry
run alongside whichever REPO_FORMAT is active, independently of it. They're
always available; there's no environment variable to enable or disable them.
| Format | Native client | Repositories/namespaces |
|---|---|---|
| APT | apt install |
Fixed set of distribution codenames |
| RPM | dnf install / zypper install |
Fixed set of distribution codenames |
| APK | apk add |
Fixed set of distribution codenames |
| Maven | mvn deploy / mvn dependency:get |
Created on first successful deploy |
| PyPI | pip install / twine upload |
Created on first successful upload |
| npm | npm publish / npm install |
Created on first successful publish |
| OCI (Docker) | docker push / docker pull |
Created on first successful push |
The OS-package formats use a fixed, admin-managed list of distributions (e.g.
jammy, almalinux9). The four newer formats use a dynamic repository
model instead — closer to how Docker Hub or a private npm registry works: the
first successful publish to a repository name creates it, no pre-registration
required. Access to a specific repository can still be restricted afterwards
(see Roles & permissions).
How each dynamic format maps onto its own repository¶
Maven resolves the standard GAV (group:artifact:version) coordinate path
— {groupId with dots as slashes}/{artifactId}/{version}/{artifactId}-{version}[-{classifier}].{extension}
— against the repository, exactly as mvn deploy/gradle publish already
send it. A single deploy publishes both a .pom and a .jar (or .war,
.aar, …) for the same version; Repod tracks them as separate entries so
neither overwrites the other. Client checksums (.sha1/.md5) and
maven-metadata.xml are accepted but never trusted as-is — Repod always
regenerates both from the artifact bytes it actually stored. SNAPSHOT
versions are rejected at publish time — release versions only.
PyPI implements the Simple Repository API (PEP 503) for pip install
and the legacy multipart upload protocol for twine upload — the same
protocol upload.pypi.org itself uses, so no custom client tooling is
needed. Project names are normalized per PEP 503 (lowercased, runs of
-/_/. collapsed to a single -) before matching, so My_Project and
my-project resolve to the same project.
npm implements the real npm registry protocol — packument GET, tarball
GET, and PUT publish with the tarball attached as base64 in the request
body, the same shape npm publish sends on the wire. Authentication reuses
Repod's own REST API token mechanism directly (Authorization: Bearer
<token>) rather than a separate login flow — paste a Repod API token into
.npmrc and npm publish/npm install work immediately, no npm login
step. Scoped packages (@scope/name) are supported for both publish and
install.
OCI (Docker/container images) is backed by an embedded Zot
registry. docker push/docker pull talk directly to Zot over the standard
Docker Registry v2 protocol — Repod itself never sits in that data path, only
in issuing the short-lived Bearer token Zot validates. See
Container Registry for the authentication
flow and deployment details.
The same pipeline, every format¶
Whichever format a package arrives as, it passes through the same sequence before it's servable:
Upload / push / publish received
│
▼
Format validation
▼
Antivirus scan
▼
CVE analysis + EPSS/KEV enrichment
▼
Policy decision — publish, or hold for review
▼
Published — or waiting in the CISO review queue
This is deliberate: a Critical CVE in a Python wheel or a container image is
exactly as dangerous as one in a .deb, and Repod treats it that way. There is
no "lightweight" path for the newer formats that skips scanning — see
Security Pipeline for the full breakdown.
For a container image, antivirus and CVE scanning run against the image's raw
layer blobs directly (each layer is just a tar.gz, scanned the same way a
.deb's ar archive or an .rpm's cpio archive already are — no manual
filesystem extraction step). Grype auto-detects the language/OS ecosystem
from the artifact itself for Maven, PyPI, npm, and OCI, so no per-format CVE
integration was needed beyond pointing it at the right file.
Digest pinning on import: importing an image from a public registry resolves the requested tag to its manifest digest once, and every later step — scan, review decision, publish — operates on that exact, immutable digest, never on the tag again. A tag can be repointed at different content later; the review decision your team recorded never silently follows it.
Dependency CVE scanning (Maven, PyPI, npm)¶
Grype scans an artifact's own bytes — the compiled .jar, the wheel, the
tarball. It has no visibility into that artifact's declared dependencies,
which get resolved separately at a consumer's build time (npm install,
mvn/gradle resolving a version range, pip install). A vulnerable
dependency named in package.json, a .pom's <dependencies> block, or a
wheel/sdist's requires_dist metadata was therefore invisible to Repod's
CVE pipeline.
A second, independent check closes that gap: Repod extracts the declared direct dependencies (no transitive resolution — a full dependency-tree walk would need either a real semver resolver or a bundled lockfile, neither of which a published artifact carries) and queries OSV.dev by package name, per ecosystem (npm, Maven, PyPI). The lookup is by name only, not by the declared version range — deliberately over-inclusive, since narrowing by an unresolved range risks a false negative more than a wide result risks noise; a human reviewer judges whether the actually-resolved version is really affected.
A dependency finding that breaches your CVE policy routes to the same review queue as a finding in the package's own code — with one difference: a policy that would otherwise block a dependency match is capped down to review instead, never an outright rejection. Unlike a CVE in the package's own bytes, a name-only dependency match is a noisier signal (a meaningful share turn out to already be fixed upstream), so it flags for human judgment rather than blocking the publish outright. A dependency finding never downgrades or overrides a stricter verdict already reached from scanning the package's own bytes.
This coverage is populated automatically for native publish/deploy
(npm publish, mvn deploy via the .pom, twine upload) and for
validated import from the public registries below, since each of those
already carries or fetches the dependency manifest. A raw manual upload with
no accompanying dependency metadata has nothing to scan for this check.
Native protocols, not a custom API¶
Every format speaks the real upstream protocol its ecosystem already uses.
mvn deploy, pip install / twine upload, npm publish, and
docker push / docker pull all work against Repod exactly as they would
against Maven Central, PyPI, npmjs.org, or Docker Hub — pointed at your Repod
URL instead, with your Repod credentials. There's nothing to install on the
client beyond the tool your team already uses.
See Client Setup for APT/RPM/Maven/PyPI/npm configuration and Container Registry for Docker.
Importing from public sources¶
Rather than requiring every artifact to be built and pushed manually, three of the four dynamic formats also support validated import: point Repod at a package on Maven Central, PyPI, npmjs.org, or a public Docker Hub image, and it downloads, scans, and publishes (or queues for review) that exact artifact — without ever bypassing the security pipeline. This is the same mechanism already used to mirror upstream security advisories for OS packages, extended to the newer formats.
Each of Maven/PyPI/npm/OCI can reach Repod's storage three ways, and all
three run through the identical validation pipeline described above: the
format's own native publish/deploy protocol (mvn deploy, twine upload,
npm publish, docker push), a manual authenticated upload for an artifact
you already have on disk, or validated import from the format's public
registry. No path skips scanning to save time — the only difference between
them is where the bytes come from.