`helm` resolves a chart name and version entirely from the repository
index before it requests any tarball. A Remote (proxy) repository built
that index only from locally-catalogued artifacts, which for a proxy is
empty, so it answered `entries: {}`: `helm repo add` and `helm repo
update` both succeeded and `helm pull`/`install`/`upgrade` could then
resolve nothing, while fetching the same tarball by full path worked.
A Virtual repository with a Remote member already merged its members'
upstream indexes, so only the direct Remote was broken.
The Remote branch now merges the upstream's index over whatever the
repository holds locally, rewriting every URL to this repository's own
`charts/` route. Locally-held entries win their (name, version), since
they carry the artifact's real stored filename -- the thing the download
route resolves before it consults upstream. With the upstream down, held
charts are still served; with nothing held, the upstream error is
surfaced (503 for an upstream 5xx, 404 for an upstream 404, 502 for a
connect failure) rather than an empty index, which would read as
"upstream publishes no charts" and let `helm repo update` report success.
Proxying a third party's document brings two robustness corrections.
Entries whose name/version cannot form one path segment are dropped, so
every URL the index advertises still resolves to the entry it came from;
encoding was rejected because an encoder covering `%` also rewrites `+`,
moving every legitimate semver-build-metadata URL. And `urls`, `created`,
`digest` and `generated` now default, so one non-conforming entry no
longer fails the whole document and takes the repository's discovery
with it.
The upstream fetch is shared with the chart-download path and moves to
the large-metadata ceiling (charts.bitnami.com's index.yaml is ~26 MiB,
over the 8 MiB default) while newly reserving against the shared
buffered-metadata budget. The reservation is held across the parse, the
merge and the render and then rides the response body, so no part of the
derived working set sits outside it -- the lifetime rpm.rs established,
now extracted into `proxy_helpers::budgeted_body` and shared.
Closes #3448
Co-authored-by: brandonrc <brandonrc@users.noreply.github.com>
|
||
|---|---|---|
| .assets | ||
| .cargo | ||
| .claude/agents | ||
| .config | ||
| .githooks | ||
| .github | ||
| .sqlx | ||
| .vex | ||
| backend | ||
| docker | ||
| docs | ||
| proof | ||
| scripts | ||
| .clippy.toml | ||
| .dockerignore | ||
| .env.example | ||
| .env.local-dev | ||
| .gitguardian.yaml | ||
| .gitignore | ||
| .jscpd.json | ||
| .mergify.yml | ||
| .trivyignore | ||
| ARCHITECTURE.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CODE_OF_CONDUCT.md | ||
| CONTRIBUTING.md | ||
| docker-compose.concurrency-e2e.yml | ||
| docker-compose.e2e-syspkg.yml | ||
| docker-compose.local-dev.yml | ||
| docker-compose.mesh-e2e.yml | ||
| docker-compose.s3-maven-test.yml | ||
| docker-compose.test.yml | ||
| docker-compose.yml | ||
| lcov.info | ||
| LICENSE | ||
| README.md | ||
| REDTEAM.md | ||
| RELEASING.md | ||
| rustfmt.toml | ||
| SECURITY.md | ||
| SETUP-CI.md | ||
| TESTING.md | ||
Artifact Keeper
An enterprise-grade, open-source artifact registry supporting 45+ package formats. Built with Rust.
Documentation | Demo | Website
Highlights
- 45+ Package Formats - Native protocol support for Maven, PyPI, NPM, Docker/OCI, Cargo, Go, Helm, and 38 more
- WASM Plugin System - Extend with custom format handlers via WebAssembly (WIT-based, Wasmtime runtime)
- Security Scanning - Automated vulnerability detection with Trivy and Grype, policy engine, quarantine workflow
- Hardened Containers - All images built on DISA STIG-approved Red Hat UBI 9 base images, non-root execution, no shell or package manager in runtime
- Borg Replication - Recursive peer mesh with swarm-based artifact distribution and P2P transfers
- Full-Text Search - OpenSearch-powered search across all repositories and artifacts
- Multi-Auth - JWT, OpenID Connect, LDAP, SAML 2.0, and API token support
- Artifactory Migration - Built-in tooling to migrate repositories, artifacts, and permissions from JFrog Artifactory
- Artifact Signing - GPG and RSA signing integrated into Debian, RPM, Alpine, and Conda handlers
System Architecture
graph LR
Client["CLI / Package Manager / Frontend"]
Backend["Backend<br/>Rust · Axum<br/>45+ format handlers"]
DB[(PostgreSQL 16)]
Storage["Storage<br/>Filesystem / S3"]
Meili["OpenSearch<br/>Full-text search"]
Trivy["Trivy<br/>Container & FS scanning"]
Grype["Grype<br/>Dependency scanning"]
OpenSCAP["OpenSCAP<br/>Compliance scanning"]
Peer1["Peer Instance"]
Peer2["Peer Instance"]
Client --> Backend
Backend --> DB
Backend --> Storage
Backend --> Meili
Backend --> Trivy
Backend --> Grype
Backend --> OpenSCAP
Backend <-->|Borg Replication| Peer1
Backend <-->|Borg Replication| Peer2
Peer1 <-->|P2P Mesh| Peer2
Backend Architecture
The backend follows a layered architecture with a middleware pipeline processing every request.
flowchart TD
REQ["HTTP Request"] --> MW["Middleware Pipeline"]
subgraph MW["Middleware"]
direction LR
CORS["CORS"] --> AUTH["Auth<br/>JWT · OIDC · LDAP<br/>SAML · API Key"]
AUTH --> RL["Rate Limiter"]
RL --> TRACE["Tracing<br/>+ Metrics"]
TRACE --> DEMO["Demo Mode<br/>Guard"]
end
MW --> ROUTER["Router<br/>50+ route groups"]
subgraph HANDLERS["Handler Layer"]
FMT["Format Handlers<br/>Maven · PyPI · NPM<br/>Docker · 41 more"]
CORE["Core Handlers<br/>Repos · Artifacts<br/>Users · Auth"]
ADV["Advanced Handlers<br/>Security · Plugins<br/>Peers · Migration"]
end
ROUTER --> HANDLERS
subgraph SERVICES["Service Layer"]
direction LR
ART["Artifact<br/>Service"]
REPO["Repository<br/>Service"]
SCAN["Scanner<br/>Service"]
PLUG["Plugin<br/>Service"]
SEARCH["Search<br/>Service"]
end
HANDLERS --> SERVICES
subgraph DATA["Data Layer"]
direction LR
PG[(PostgreSQL)]
FS["Storage<br/>FS / S3"]
MS["OpenSearch"]
SC["Trivy / Grype / OpenSCAP"]
end
SERVICES --> DATA
Supported Package Formats
45+ formats organized by ecosystem. Each has a native protocol handler that speaks the package manager's wire protocol.
Languages & Runtimes
| Format | Aliases | Ecosystem |
|---|---|---|
| Maven | Gradle | Java, Kotlin, Scala |
| NPM | Yarn, Bower, pnpm | JavaScript, TypeScript |
| PyPI | Poetry, Conda | Python |
| NuGet | Chocolatey, PowerShell | .NET, C# |
| Cargo | Rust | |
| Go | Go modules | |
| RubyGems | Ruby | |
| Hex | Elixir, Erlang | |
| Composer | PHP | |
| Pub | Dart, Flutter | |
| CocoaPods | iOS, macOS | |
| Swift | Swift Package Manager | |
| CRAN | R | |
| SBT | Ivy | Scala, Java |
Containers & Infrastructure
| Format | Aliases | Ecosystem |
|---|---|---|
| Docker / OCI | Podman, Buildx, ORAS, WASM OCI, Helm OCI | Container images |
| Helm | Kubernetes charts | |
| Terraform | OpenTofu | Infrastructure modules |
| Vagrant | VM boxes |
System Packages
| Format | Ecosystem |
|---|---|
| RPM | RHEL, Fedora, CentOS |
| Debian | Ubuntu, Debian |
| Alpine | Alpine Linux (APK) |
| Conda | Conda channels |
| OPKG | OpenWrt, embedded Linux |
Configuration Management
| Format | Ecosystem |
|---|---|
| Chef | Chef Supermarket |
| Puppet | Puppet Forge |
| Ansible | Ansible Galaxy |
ML / AI
| Format | Ecosystem |
|---|---|
| HuggingFace | Models, datasets |
| ML Model | Generic ML artifacts |
Editor Extensions
| Format | Aliases | Ecosystem |
|---|---|---|
| VS Code | Extension marketplace (VS Code, Cursor, Windsurf, Kiro) | |
| JetBrains | Plugin repository |
Schemas
| Format | Ecosystem |
|---|---|
| Protobuf / BSR | Buf Schema Registry, Connect RPC |
Other
| Format | Ecosystem |
|---|---|
| Conan | C, C++ |
| Git LFS | Large file storage |
| Bazel | Bazel modules |
| P2 | Eclipse plugins |
| Generic | Any file type |
Custom formats can be added via the WASM plugin system.
Security Scanning Pipeline
Every artifact upload is automatically scanned for known vulnerabilities.
flowchart LR
UP["Artifact<br/>Upload"] --> HASH{"SHA-256<br/>Dedup"}
HASH -->|New artifact| T["Trivy<br/>FS Scanner"]
HASH -->|New artifact| G["Grype<br/>Dependency Scanner"]
HASH -->|Already scanned| CACHE["Cached<br/>Results"]
T --> SCORE["Vulnerability<br/>Score A-F"]
G --> SCORE
CACHE --> SCORE
SCORE --> POL{"Policy<br/>Engine"}
POL -->|Pass| OK["Stored"]
POL -->|Fail| Q["Quarantined"]
- Dual scanner - Trivy for filesystem/container analysis, Grype for dependency trees
- Scoring - A through F grades based on finding severity and count
- Policies - Configurable rules that block or quarantine artifacts
- Signing - GPG/RSA signing for Debian, RPM, Alpine, and Conda packages
Container-image scanning (
TRIVY_ADAPTER_URL). The basedocker-compose.ymlwiresTRIVY_URLfor the legacy trivy server (filesystem / incus rootfs scanning) only. To get first-class Trivy container-image reports, setTRIVY_ADAPTER_URL(e.g.http://scanner-adapter:8080) so the backend registers the dedicatedImageScanneragainst the in-reposcanner-adapter(Harbor Pluggable Scanner API). Without it, images are still covered by grype (registry mode) but no Trivy image report is produced. Uncomment theTRIVY_ADAPTER_URLline and thescanner-adapterservice indocker-compose.ymlto enable it.The adapter pulls images back from the registry, so it must be told an address of the backend that is reachable from the adapter container —
localhostis not, since the adapter itself listens on:8080in its own network namespace (#3169). When nothing is configured the backend now advertises its own container address automatically; setTRIVY_ADAPTER_REGISTRY_URL(or the sharedAK_GRYPE_REGISTRY_HOST) to override, e.g.http://backend:8080. The automatic derivation is skipped when the adapter's own URL is a loopback address, because that normally means the adapter shares this network namespace (cargo runnext to a local adapter, ornetwork_mode: host). It is not skipped correctly for an adapter container published on loopback (docker run -p 127.0.0.1:8081:8080 …scanner-adapter): there the backend reaches the adapter over loopback but the adapter cannot reach the backend that way, so setTRIVY_ADAPTER_REGISTRY_URLexplicitly. The backend logs which fallback it took on every scan. Give the adapter its own trivy cache volume — reusing the trivy server's root-ownedtrivy_cachefails withpermission deniedonfanal/fanal.dbbecause the adapter runs unprivileged (UID 1001).Not-applicable scanners. A scanner that does not apply to an artifact's format (e.g. the filesystem/incus/openscap scanners on a Docker image) records a
not_applicableresult — a benign terminal state, distinct fromfailed. The scan-list API folds multiplenot_applicableresults for the same artifact into a single summary row (collapsed_not_applicable_count+collapsed_scan_types) so they read as one muted "not applicable" indication rather than N failures.
Borg Replication
Recursive peer-to-peer replication where every node is a full Artifact Keeper instance. No thin caches — each peer runs the same stack and can serve as an origin for other peers.
graph TD
P1["Peer<br/>US-West"]
P2["Peer<br/>EU-Central"]
P3["Peer<br/>AP-Southeast"]
P4["Peer<br/>US-East"]
P1 <-->|"Chunked Transfer"| P2
P1 <-->|"Chunked Transfer"| P4
P2 <-->|"Chunked Transfer"| P3
P3 <-->|"Chunked Transfer"| P4
P1 <-->|"P2P Mesh"| P3
P2 <-->|"P2P Mesh"| P4
- Recursive peers - Every peer is a full instance (backend, DB, storage) that can originate replication to other peers
- Swarm-based distribution - Artifacts replicate across the mesh based on demand
- Chunked transfers - Large artifacts split for reliable delivery over unstable links
- Network-aware scheduling - Bandwidth and latency profiling for optimal routing
WASM Plugin System
Extend Artifact Keeper with custom format handlers compiled to WebAssembly.
- WIT-based interface - Plugins implement a well-defined
FormatHandlercontract - Wasmtime runtime - Sandboxed execution with fuel-based CPU limits and memory caps
- Hot reload - Install, enable, disable, and reload plugins without restart
- Sources - Load from Git repositories or ZIP uploads
Quick Start
Get running in 5 minutes with Docker Compose: Quickstart Guide
Documentation
- Quickstart — Get running in 5 minutes
- Installation — Docker Compose, Windows Service (beta), or build from source
- Configuration — Environment variables reference
- Package Formats — All 45+ supported formats
- Docker Deployment — Production setup guide
Project Structure
artifact-keeper/
├── backend/ # Rust backend (Axum, SQLx, 6,400+ unit tests)
│ ├── src/
│ │ ├── api/ # Handlers, middleware, routes
│ │ ├── formats/ # 45+ format handler implementations
│ │ ├── services/ # Business logic (68 services)
│ │ ├── models/ # Data models (21 types)
│ │ └── storage/ # FS and S3 backends
│ └── migrations/ # 69 PostgreSQL migrations
├── edge/ # Peer replication service (Rust)
├── scripts/ # Test runners, native client tests, stress tests
└── .github/ # CI/CD workflows
Technology Choices
| Layer | Choice | Why |
|---|---|---|
| Backend language | Rust | Memory safety, performance, strong type system |
| Web framework | Axum | Tower middleware ecosystem, async-first |
| Database | PostgreSQL 16 | JSONB for metadata, mature ecosystem |
| Search | OpenSearch | Fast full-text search, easy to operate |
| Security scanning | Trivy + Grype + OpenSCAP | Complementary coverage, industry standard |
| Plugin runtime | Wasmtime | Sandboxed, portable, WIT contract system |
| Storage | Filesystem / S3 | Simple default, cloud-ready upgrade path |
CI/CD Pipeline
Seven GitHub Actions workflows handle testing, publishing, and deployment.
flowchart TD
subgraph TRIGGER["Triggers"]
PUSH["Push / PR<br/>to main"]
TAG["Tag v*"]
CRON["Daily 2 AM UTC"]
SITE_PUSH["Push to site/**"]
end
subgraph CI["ci.yml — Every Push/PR"]
direction TB
LINT["🦀 Lint Rust<br/>fmt + clippy"]
UNIT["🧪 Unit Tests<br/>cargo test --lib"]
INTEG["🔗 Integration Tests<br/>+ PostgreSQL<br/>(main push only)"]
SMOKE["🔥 Smoke E2E<br/>PyPI · npm · Cargo<br/>docker-compose.test.yml"]
AUDIT["🔒 Security Audit<br/>cargo audit"]
CI_OK["✅ CI Complete"]
LINT --> UNIT
LINT --> INTEG
UNIT --> SMOKE
SMOKE --> CI_OK
AUDIT --> CI_OK
end
subgraph DOCKER["docker-publish.yml — Push to main / tags"]
direction TB
BE_BUILD["Backend<br/>amd64 + arm64"]
OS_BUILD["OpenSCAP<br/>amd64 + arm64"]
BE_MERGE["Multi-Arch<br/>Manifest"]
OS_MERGE["Multi-Arch<br/>Manifest"]
BE_BUILD --> BE_MERGE
OS_BUILD --> OS_MERGE
end
subgraph E2E["e2e.yml — Manual / called by release"]
direction TB
PKI["🔐 Setup PKI<br/>TLS + GPG"]
NATIVE["📦 Native Client Tests<br/>10 formats"]
STRESS["🔥 Stress Tests<br/>100 concurrent uploads"]
FAILURE["💥 Failure Tests<br/>crash · db · storage"]
PKI --> NATIVE
NATIVE --> STRESS
NATIVE --> FAILURE
end
subgraph RELEASE["release.yml — Tags v*"]
direction TB
E2E_GATE["🚦 E2E Gate<br/>all formats + stress + failure"]
BINARIES["📦 Build Binaries<br/>linux + macOS<br/>amd64 + arm64"]
GH_RELEASE["🚀 GitHub Release<br/>binaries + checksums"]
E2E_GATE --> BINARIES
BINARIES --> GH_RELEASE
end
subgraph NIGHTLY["scheduled-tests.yml — Daily"]
direction TB
NIGHTLY_E2E["🌙 Nightly Smoke E2E"]
DEP_CHECK["🔍 Dependency Check"]
SEC_SCAN["🔒 Security Scan"]
end
subgraph SITE["site.yml"]
PAGES["📄 Build + Deploy<br/>GitHub Pages"]
end
subgraph AMI["ami-build.yml"]
PACKER["🖥️ Packer Build AMI"]
end
PUSH --> CI
PUSH --> DOCKER
TAG --> RELEASE
TAG --> DOCKER
CRON --> NIGHTLY
SITE_PUSH --> SITE
GH_RELEASE -.->|"on release published"| AMI
classDef trigger fill:#6f42c1,color:#fff,stroke:#6f42c1
classDef ci fill:#2ea44f,color:#fff,stroke:#2ea44f
classDef docker fill:#0969da,color:#fff,stroke:#0969da
classDef release fill:#d97706,color:#fff,stroke:#d97706
class PUSH,TAG,CRON,SITE_PUSH trigger
class LINT,UNIT,INTEG,SMOKE,AUDIT,CI_OK ci
class BE_BUILD,OS_BUILD,BE_MERGE,OS_MERGE docker
class E2E_GATE,BINARIES,GH_RELEASE release
| Workflow | Trigger | What It Does |
|---|---|---|
| ci.yml | Every push/PR | Lint, unit tests, integration tests, smoke E2E (PyPI, npm, Cargo) |
| docker-publish.yml | Push to main, tags | Multi-arch Docker images (backend + OpenSCAP) to ghcr.io |
| e2e.yml | Manual or called by release | Full E2E: 10 native client formats, stress, failure injection |
| release.yml | Tags v* |
E2E gate, cross-platform binaries, GitHub Release |
| scheduled-tests.yml | Daily 2 AM UTC | Nightly smoke E2E, dependency check, security scan |
| site.yml | Push to site/** |
Build and deploy docs to GitHub Pages |
| ami-build.yml | On release published | Bake AWS AMI with Packer |
Sponsors
Thank you to our sponsors for supporting ongoing development of Artifact Keeper.
Backers
![]() Ash A. |
![]() Gabriel Rodriguez |
Become a sponsor to support the project and get your name listed here.
Contributing
We welcome contributions! See CONTRIBUTING.md for guidelines.
Have questions or ideas? Join the conversation in GitHub Discussions.
License
MIT License - see LICENSE for details.
Built with Rust. "JFrog" and "Artifactory" are trademarks of JFrog Ltd. Artifact Keeper is not affiliated with or endorsed by JFrog.

