Git caching reverse proxy with bundle-uri support.
forgeproxy sits between Git clients and an upstream forge (GitHub Enterprise, GitHub.com, GitLab, Gitea, or Forgejo). It maintains bare-repo caches on local disk, serves clones and fetches from cache, and generates Git bundles that clients can download over HTTP before negotiating a pack — dramatically reducing load on the upstream forge and speeding up clones.
- SSH and HTTP transport — proxy both
git clone ssh://andgit clone https:// - Bundle-URI — automatic generational bundle publication from freshly updated mirrors; clients that support
bundle-urifetch most data from pre-built bundles - Filtered bundles — optional blobless / treeless bundle variants for partial-clone workflows
- Multi-forge support — pluggable backend for GitHub Enterprise, GitHub.com, GitLab, Gitea, and Forgejo
- Distributed coordination — Valkey/Redis-backed locks, pub/sub invalidation, and node registry for multi-node deployments
- Adaptive fetch scheduling — background re-fetch interval adapts to repo activity with exponential backoff for idle repos
- Two-tier cache eviction — local disk cache with configurable LRU or LFU eviction and high/low water marks
- S3 bundle storage — bundles are persisted to S3 (with optional FIPS endpoints) and served via pre-signed URLs
- Linux kernel keyring credentials — upstream PATs and SSH keys stored in the kernel keyring via
linux-keyutils - Auth caching — SSH fingerprint and HTTP token auth results cached in Valkey with configurable TTLs
- Webhook-driven invalidation — push webhooks trigger immediate re-fetch of affected repos
- Forge API rate-limit awareness — self-throttles when the upstream API rate-limit budget runs low
- Adaptive runtime tuning — optional AIMD or demand/resource controllers adjust runtime concurrency, thread, and request-wait budgets within operator bounds
- Prometheus metrics — counters, gauges, and histograms for clone latency, cache hit rate, bundle generation, and more
- FIPS 140 build — optional
fipsCargo feature for FIPS-validated TLS via rustls - NixOS module — declarative deployment with systemd hardening out of the box
- Comprehensive NixOS VM tests — 10 integration tests covering basic operation, SSH authz, secrets, cache eviction, compliance, and more
┌───────────┐ SSH (2222) ┌──────────────┐ SSH / HTTPS ┌───────────┐
│ │ ────────────────────▶ │ │ ──────────────────────▶ │ │
│ Git │ HTTPS (8080) │ forgeproxy │ │ Upstream │
│ Clients │ ────────────────────▶ │ │ ◀── push webhook ─────── │ Forge │
│ │ ◀── bundle-uri ────── │ │ │ │
└───────────┘ └──────┬───────┘ └───────────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Local │ │ Valkey │ │ S3 │
│ Disk │ │ Redis │ │Bundles │
│ Cache │ │ │ │ │
└────────┘ └────────┘ └────────┘
| Backend | SSH key resolution | Notes |
|---|---|---|
github-enterprise |
Yes | Requires site_admin PAT scope on GHE |
github |
No | GitHub.com has no admin key-lookup API |
gitlab |
Yes | Requires self-managed instance admin token |
gitea |
Yes | Requires instance admin token |
forgejo |
Yes | Same API as Gitea; different webhook headers |
When SSH key resolution is unavailable, clients must authenticate via HTTP token rather than SSH key passthrough.
Build with Nix:
nix build .#forgeproxy # standard build
nix build .#forgeproxy-fips # FIPS 140 TLS buildCopy and edit the example configuration:
cp config.example.yaml /run/forgeproxy/config.yaml
# edit upstream hostname, API URL, Valkey endpoint, S3 bucket, etc.Run:
forgeproxy --config /run/forgeproxy/config.yamlSee config.example.yaml for a fully commented example.
Top-level sections:
| Section | Purpose |
|---|---|
upstream |
Forge hostname, API URL, admin token env var |
backend_type |
Forge flavour (github-enterprise, github, gitlab, etc.) |
upstream_credentials |
Default credential mode and per-org overrides (PAT / SSH) |
proxy |
SSH and HTTP listen addresses |
valkey |
Valkey/Redis endpoint, TLS, auth token |
auth |
SSH/HTTP auth cache TTLs, webhook secret env var |
clone |
Freshness threshold, lock TTLs, concurrency semaphores |
fetch_schedule |
Background re-fetch interval, backoff, rolling window |
bundles |
Consolidation schedule, min clone count, filtered bundles |
storage |
Local disk path/percentages/eviction policy, S3 bucket/region/FIPS |
delegated_repositories |
Repositories always delegated to upstream instead of local cache |
repo_overrides |
Per-repo overrides for fetch interval, freshness, bundles |
The flake exports a NixOS module for declarative deployment:
{
inputs.forgeproxy.url = "github:your-org/forgeproxy";
outputs = { self, nixpkgs, forgeproxy, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
{ nixpkgs.overlays = [ forgeproxy.overlays.default ]; }
forgeproxy.nixosModules.forgeproxy
{
services.forgeproxy = {
enable = true;
configFile = "/run/forgeproxy/config.yaml";
logLevel = "info";
};
}
];
};
};
}Additional NixOS modules are available for companion services:
forgeproxy.nixosModules.valkey— Valkey instanceforgeproxy.nixosModules.nginx— nginx TLS terminationforgeproxy.nixosModules.hardening— extra systemd hardeningforgeproxy.nixosModules.secrets— sops-nix secrets integrationforgeproxy.nixosModules.security-controls— security control profiles (regulated, SOC2)
Prometheus metrics are exposed at GET /metrics. A health check endpoint is
available at GET /healthz.
When forgeproxy's local observability config and the collector-specific OTLP config enable any signal export, the forgeproxy host also runs a local OpenTelemetry Collector. Forgeproxy and the collector now have separate config surfaces:
config.yaml: forgeproxy-owned observability toggles and local behaviorotel-collector-config.yaml: collector-owned host metrics and OTLP egress
The signal flow is:
- Metrics: forgeproxy exposes Prometheus/OpenMetrics at
/metrics; the host-local Collector scrapes that endpoint and re-exports it as OTLP. Ifmetrics.host.enabledis true in the collector config, the same Collector also emits host CPU, disk, filesystem, load, memory, network, and paging metrics from the forgeproxy node itself. - Logs: forgeproxy continues to write structured logs to journald; the
host-local Collector tails
forgeproxy.serviceand exports those logs as OTLP. - Traces: forgeproxy emits tracing spans through Rust
tracing; whenobservability.traces.enabledis true, forgeproxy sends those spans to the host-local Collector over a fixed loopback OTLP receiver at127.0.0.1:4317, and the Collector exports them onward.
At service startup, forgeproxy also performs best-effort runtime environment
detection and writes a shared resource-attributes file under
/run/forgeproxy/runtime-resource-attributes.json. Both forgeproxy itself and
the host-local Collector reuse that file so all three signals share the same
resource identity. On AWS this uses IMDSv2; Azure and GCP metadata detection is
also attempted. If cloud metadata is indeterminate, startup continues and
forgeproxy logs a warning before falling back to local identifiers such as
/etc/machine-id.
That means the internal Collector endpoint and the external backend endpoints are different things:
- Internal endpoint: a private loopback hop used only on the forgeproxy host so forgeproxy can hand trace spans to the Collector.
- External endpoints: the real egress destinations configured in
otel-collector-config.yaml.
The normal forgeproxy configuration shape is:
observability:
metrics:
prometheus:
enabled: true
refresh_interval_secs: 60
logs:
journald:
enabled: true
traces:
enabled: true
sample_ratio: 1.0The matching collector configuration shape is:
metrics:
host:
enabled: false
exporters:
otlp:
metrics:
enabled: true
endpoint: "https://collector.internal.example/v1/metrics"
protocol: "http/protobuf"
auth:
basic:
username: "metrics-user"
password: "metrics-password"
logs:
enabled: true
endpoint: "https://logs.internal.example/v1/logs"
protocol: "http/protobuf"
traces:
enabled: true
endpoint: "https://traces.internal.example/v1/traces"
protocol: "http/protobuf"Each signal can use a different OTLP endpoint, protocol, and basic-auth credential pair in the collector config. That is the intended way to point the on-host Collector at an internal Collector or auth proxy which then forwards to the final backend, such as a VictoriaMetrics metrics ingest URL plus different log/trace backends.
The Collector also upserts these runtime resource attributes onto metrics, logs, and traces when it exports them:
service.instance.id: cloud instance ID when available, otherwise a best-effort stable fallbackservice.machine_id:/etc/machine-idservice.ip_address: best-effort source IP for the node's outbound interfacecloud.provider,cloud.platform,cloud.region: when detected from AWS, Azure, or GCP metadata
Host metrics are controlled separately from forgeproxy's own /metrics
endpoint:
observability.metrics.prometheus.enabled: expose forgeproxy application metrics locally at/metricsso the on-host Collector can scrape them.observability.metrics.prometheus.refresh_interval_secs: refresh cache usage gauges such as mirror sizes and cache subtree sizes in the forgeproxy process. Defaults to 60 seconds.metrics.host.enabledinotel-collector-config.yaml: collect host-level system metrics from the forgeproxy node through the Collector'shostmetricsreceiver.exporters.otlp.metrics.enabledinotel-collector-config.yaml: actually export whichever metrics sources were enabled above.
The current host metrics include CPU, disk I/O, filesystem capacity and usage,
load average, memory usage, paging activity, and network traffic. The OTLP
metric names for those come from the Collector's hostmetrics receiver, for
example system.cpu.time, system.memory.usage, system.filesystem.usage,
and system.network.io.
Enter the dev shell:
nix developRun Rust tests:
cd rust && cargo testRun the full check suite (formatting, clippy, and all NixOS VM tests):
nix flake check