AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
翻译准备中
High-performance reverse proxy and file server in Rust. Built on Cloudflare Pingora. HTTP/2, caching, load balancing, TLS, hot reload, Prometheus — single binary, JSON config.
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
Production-grade reverse proxy and API gateway built on Cloudflare Pingora. A single binary, single config file that handles proxying, static files, auth, rate limiting, caching, scripting, and TLS — with a 14 MB footprint, ~28 ms cold start, and zero runtime dependencies. Minimal hand-written config — YAML and JSON are equivalent: → Full examples: examples/ · → Configuration reference: docs/configuration.md Proxying Reverse proxy with 8 load-balancing strategies, healt [README 설치·사용 부분 발췌] Installation查看完整 README ↗
Conduit is a reverse proxy, API gateway and static file server written in Rust, built on
Cloudflare Pingora. You describe your sites, routes,
authentication, limits and caching in one YAML or JSON file and run one executable. Conduit
terminates TLS, balances and protects your upstreams, serves a single-page app next to its API,
proxies raw TCP, lets you add your own logic with scripts and plugins, and picks up configuration
changes without dropping connections. It runs from a config file, from a Docker image, or on
Kubernetes, where every ConduitSite resource becomes a site and changes apply on their own.
npx @lopatnov/conduit init # write a starter conduit.yaml
npx @lopatnov/conduit # run it
Other ways to run it: npm install -g @lopatnov/conduit · pre-built binaries for
Linux, macOS and Windows · the Docker image ghcr.io/lopatnov/conduit · cargo install lopatnov-conduit ·
Kubernetes with ConduitSite resources.
Details: Installation.
conduit init # interactive wizard
conduit init --yes # non-interactive: accept the defaults and write conduit.yaml
conduit validate # check the config
conduit # start
A minimal hand-written config. YAML and JSON are equivalent:
# conduit.yaml
port: 8080
static: ./dist
proxy:
/api: http://localhost:4000
curl http://localhost:8080/__health__
# {"status":"ok"}
→ Ready-to-run configs: examples/ · → Every field: docs/configuration.md
· → Other ways to install: Installation
Where each group is documented in detail:
| Group | Read |
|---|---|
| Routing, redirects, path rewrite | Routes · Redirects · URL rewriting |
| Proxy and balancing | Proxy · Load balancing · Sticky sessions · Mirroring · Upstream TLS · Connection pool |
| Resilience | Health checks · Circuit breaker · Retry · Outlier detection |
| Caching | Proxy cache |
An asterisk on the map means the feature is optional at build time. Choose your build shows which build has what.
Guards run in a fixed order, so you can reason about what happens first: a client blocked by the IP
filter never reaches rate limiting or auth, and a request that fails auth never reaches your
scripts or your upstream. Routing then picks the first matching route, and a handler serves it.
/__health__ is answered at the end of the gate stage, before the host check, limits and auth.
Each block is a complete config you can save as conduit.yaml and check with conduit validate.
Features marked (needs x) are optional at build time; see Choose your build.
port: 8080
proxy:
/api:
targets:
- http://api-1:4000
- http://api-2:4000
- http://api-3:4000
strategy: least-conn # also round-robin, p2c, ip-hash, consistent-hash …
healthCheck: { path: /health, intervalSecs: 10 }
maxConnectionsPerUpstream: 200 # 503 instead of queueing when every target is full
retry: { attempts: 2, conditions: [connection_error, 5xx] }
outlierDetection: { consecutive5xx: 5 } # eject a target that keeps failing real requests
→ Details: Load balancing · Health checks · Circuit breaker · Retry
port: 8080
proxy:
/api:
targets: [https://api-1.internal:8443, https://api-2.internal:8443]
backup: https://api-standby.internal:8443 # used while every target is unhealthy
upstreamTls: { verify: true } # on by default; checks the certificate against the host in the target URL
http2: true # HTTP/2 to the upstream
mirror: http://api-v2:4000 # shadow copy of each request (headers only); the reply is dropped
websocket: true # allow WebSocket upgrades on this route
The mirror receives a copy of the request headers, Authorization and cookies included, so mirror
only to a service you trust. Upstream certificates are always checked against the host in the target
URL, so write the name the certificate carries; upstreamTls.serverName does not change that yet
(#583).
→ Details: Backup targets ·
Upstream TLS ·
Mirroring ·
Connection pool
port: 8080
static: ./dist
fallback: { file: ./dist/index.html, status: 200 } # unknown paths get the app shell
compression: true
proxy:
/api: http://localhost:4000
→ Details: Static files · Fallback · Compression
port: 8080
static: ./dist
compression: true # br, zstd or gzip: static files, fallback pages and metrics
securityHeaders: true # nosniff, frame, referrer and XSS headers; HSTS is opt-in
cors: { origins: ["https://app.example.com"] }
maskErrors: true # 5xx bodies from upstreams become a generic JSON error
responseTransform:
setHeaders: { X-Served-By: conduit }
removeHeaders: [Server]
proxy:
/api: http://localhost:4000
→ Details: Compression · Security headers · CORS · Error masking · Response transform
acme)host: example.com
port: 443
http2: {}
tls:
acme: { email: [email protected] } # Let's Encrypt; it validates over port 80
httpRedirectPort: 80 # plain HTTP is redirected to HTTPS
proxy: http://localhost:4000
Bring your own certificate instead with tls: { cert: ./fullchain.pem, key: ./privkey.pem }.
Conduit renews ACME certificates in the background and writes them to disk, but a running process
keeps serving the certificate it loaded at start, so restart it within 30 days of expiry (any
deploy does it).
→ Details: TLS / HTTPS ·
mTLS
port: 8080
jwtAuth: # needs jwt
jwksUrl: https://auth.example.com/.well-known/jwks.json
audience: [my-api]
requestTransform:
setHeaders:
X-User-ID: "{{ jwt.sub }}" # pass a claim on to your service
proxy:
/api: http://localhost:4000
Prefer something simpler? basicAuth: { users: { alice: "$ALICE_PASSWORD" } } and
apiKey: { keys: ["$API_KEY"], header: X-API-Key } work the same way, and forwardAuth hands the
decision to your own service. → Details: Basic ·
API key · JWT ·
Forward auth · Consumers
port: 8080
rateLimit: { windowSecs: 60, limit: 300 } # per client IP, for the whole site
proxy:
/api/payments:
targets: [http://payments:4000]
rateLimit: { windowSecs: 60, limit: 10 } # a stricter limit for this route, also per client IP
Limits are counted per client IP unless you set keyBy: "header:X-Name". A header is chosen by the
client and can be forged, so only key on one that a trusted proxy in front of Conduit sets. For
quotas per authenticated client use consumers. Add
store: "redis://host:6379" to share counters between instances (needs redis).
→ Details: Rate limiting
cache)port: 8080
proxy:
/api:
targets: [http://backend:4000]
cache:
store: memory # or redis://… or disk:/path
ttlSecs: 60
staleWhileRevalidateSecs: 300 # serve a stale copy while refreshing in the background
staleIfErrorSecs: 600 # and when the upstream is down
skipIfCookie: true # never cache requests that carry a cookie
→ Details: Proxy cache
port: 8080
middleware:
- type: script # Rhai, needs rhai
path: ./scripts/auth-check.rhai
- type: wasm # WebAssembly, needs wasm
path: ./plugins/my-plugin.wasm
proxy:
/api: http://localhost:4000
// scripts/auth-check.rhai: deny requests without an Authorization header
let token = request.header("Authorization");
if token == "" {
response.status = 401;
response.body = "Unauthorized";
return false;
}
true
Scripts and plugins fail open, so keep your real access checks in the auth guards. → Guides: Rhai · WebAssembly
sites:
- port: 8080
logging: json # structured access log; every request gets an X-Request-ID
metrics: { path: /__metrics__, token: "$METRICS_TOKEN" } # Prometheus
responseTime: true # adds an X-Response-Time header (milliseconds)
serverTiming: true # Server-Timing: total and upstream, for browser DevTools
proxy: http://localhost:4000
global:
otlp: { endpoint: "http://tempo:4317", serviceName: my-api, sampleRate: 0.1 } # needs otlp
→ Details: Logging · Metrics · OpenTelemetry · Response time · Server-Timing
sites:
- host: app.example.com
port: 443
tls: { acme: { email: [email protected] } }
static: ./dist
proxy: { /api: http://api:4000 }
- host: admin.example.com
port: 443
tls: { acme: { email: [email protected] } }
basicAuth: { users: { admin: "$ADMIN_PASSWORD" } }
proxy: http://admin-backend:5000
ConduitSite per site (needs kubernetes)Instead of a config file, Conduit can read its sites from Kubernetes custom resources. Each
ConduitSite is one virtual site, and its spec takes the same fields as a site in conduit.yaml.
# site.yaml
apiVersion: conduit.io/v1
kind: ConduitSite
metadata:
name: my-app
namespace: default
spec:
port: 8080
host: app.example.com
proxy:
/api:
targets: [http://my-svc:4000]
healthCheck: { path: /health }
rateLimit: { windowSecs: 60, limit: 500 }
kubectl apply -f contrib/k8s/conduitsite-crd.yaml # once per cluster: the ConduitSite definition
kubectl apply -f site.yaml # add or change a site
conduit --kubernetes-namespace default # in the Conduit pod; '*' watches all namespaces
Conduit watches the namespace, combines the resources into one config, checks it and swaps it in
without a restart; an update that fails validation is rejected and the running config stays. Use
the :latest-full image (or a build with --features kubernetes) and give the pod a service
account that may get, list and watch conduitsites. Write access to ConduitSite objects is
deploy access: spec takes any site field, file paths such as static and upload.dir included, so
grant it only to people who may deploy to the Conduit pod.
→ CRD, RBAC and Deployment manifests:
docs/deployment.md
tcp) and file upload (needs upload)sites:
- port: 3306 # raw passthrough with no auth layer: restrict who can reach this port
tcp:
targets: ["mysql-primary:3306", "mysql-replica:3306"]
strategy: round-robin # or random
- port: 8080
upload:
path: /upload # multipart POST; files get generated names
dir: ./uploads
maxFileSizeBytes: 5242880
allowedMimeTypes: [image/png, application/pdf]
Uploads are open to anyone who can reach the port unless the site also has an auth guard
(basicAuth, apiKey or jwtAuth).
→ Details: TCP proxy · Upload
→ More scenarios: docs/recipes.md covers HTTPS, load balancing, failover,
circuit breaker, caching, security hardening, observability and Kubernetes, and
examples/ has a validated config for each.
npx @lopatnov/conduit init # generate config
npx @lopatnov/conduit # start the server
npx @lopatnov/conduit validate # validate config
npm install -g @lopatnov/conduit
conduit validate
conduit
The npm package downloads the native binary for your platform from
GitHub Releases when it is installed (set
CONDUIT_SKIP_DOWNLOAD=1 to skip that). Node.js is only needed for the download and for the
conduit launcher script; the server itself is the Rust binary. Since 2.0.0 the package installs
the full build; platforms: Linux x64 and arm64 (glibc), macOS Intel and Apple Silicon,
Windows x64. See the npm package page for details.
Download from GitHub Releases:
| Platform | Standard | Full |
|---|---|---|
| Linux x86-64 | conduit-x86_64-unknown-linux-gnu.tar.gz | conduit-x86_64-unknown-linux-gnu-full.tar.gz |
| Linux x86-64 musl | conduit-x86_64-unknown-linux-musl.tar.gz | conduit-x86_64-unknown-linux-musl-full.tar.gz |
| Linux ARM64 | conduit-aarch64-unknown-linux-gnu.tar.gz | conduit-aarch64-unknown-linux-gnu-full.tar.gz |
| Linux RISC-V 64 | conduit-riscv64gc-unknown-linux-gnu.tar.gz | — |
| macOS Intel | conduit-x86_64-apple-darwin.tar.gz | conduit-x86_64-apple-darwin-full.tar.gz |
| macOS Apple Silicon | conduit-aarch64-apple-darwin.tar.gz | conduit-aarch64-apple-darwin-full.tar.gz |
| Windows x86-64 | conduit-x86_64-pc-windows-msvc.exe.zip | conduit-x86_64-pc-windows-msvc-full.exe.zip |
# Linux x86-64, standard
curl -L https://github.com/lopatnov/conduit/releases/latest/download/conduit-x86_64-unknown-linux-gnu.tar.gz \
| tar xz && ./conduit-x86_64-unknown-linux-gnu --version
Every release also carries SHA256SUMS.txt (it lists every file attached to the release,
archives included) and GitHub build-provenance attestations for the bare binaries, which are
attached next to the archives (gh attestation verify <binary> --repo lopatnov/conduit; the
archives themselves are covered by the checksums only). The musl builds are statically linked;
the glibc builds link the system C library dynamically.
cargo install lopatnov-conduit # default build
cargo install lopatnov-conduit --features standard # + JWT, consumers, forward auth, cache, ACME
cargo install lopatnov-conduit --features full # everything
# or pick the features you need
cargo install lopatnov-conduit --features "jwt,cache,rhai" # default build + JWT auth, caching, Rhai scripts
cargo install lopatnov-conduit --features "tcp,redis,otlp" # default build + TCP proxy, Redis, OpenTelemetry
cargo install lopatnov-conduit --no-default-features --features proxy # reverse proxy only
cargo install lopatnov-conduit --no-default-features --features static-server # static files only, no proxy
cargo install lopatnov-conduit --no-default-features --features gateway # proxy, auth, cache, ACME; no static files
For build instructions, feature bundles, cross-compilation and troubleshooting see docs/building.md.
docker pull ghcr.io/lopatnov/conduit:latest # standard build
docker pull ghcr.io/lopatnov/conduit:latest-full # full build
docker run -p 8080:8080 \
-v $(pwd)/conduit.yaml:/etc/conduit/conduit.yaml:ro \
ghcr.io/lopatnov/conduit:latest -c /etc/conduit/conduit.yaml
The images are FROM scratch (a static musl binary, no shell, no OS userland) and run as UID
65534. On Kubernetes, use the :latest-full image and ConduitSite resources:
see the example above.
→ docker-compose, systemd, Kubernetes and a production checklist:
docs/deployment.md
| You install with | You get |
|---|---|
npx / npm install -g | full |
| GitHub Release archives | standard, or full (files ending in -full) |
| Docker | :latest is standard, :latest-full is full |
cargo install lopatnov-conduit | the default build; add --features … for more |
If your config uses a feature your binary was built without, conduit validate and the start-up
log say so instead of silently ignoring it. To go the other way and find out what a config
needs:
$ conduit features -c conduit.yaml
required: proxy
build: cargo install lopatnov-conduit --no-default-features --features proxy
bundle: `--features gateway` also covers this configuration
| Feature | What it enables |
|---|---|
jwt | JWT Bearer-token auth and JWKS URLs (jwtAuth) |
consumers | Named API clients with per-consumer credentials and rate limits |
forward-auth | Delegate auth to an external HTTP service (forwardAuth) |
rhai | Rhai scripting middleware (type: "script") |
wasm | WebAssembly plugin middleware (type: "wasm") through Wasmtime |
tcp | Raw TCP proxy mode (type: "tcp" site) |
upload | Multipart file upload handler (upload: site config) |
redis | Redis-backed rate limiting and caching |
cache | Response caching (proxy.*.cache); implies proxy |
disk-cache | Disk-backed cache store (cache.store: "disk:/path") |
acme | Automatic certificates from Let's Encrypt (tls.acme) |
fault-injection | Fault injection for chaos testing |
otlp | OpenTelemetry OTLP tracing (global.otlp) |
tokio-metrics | conduit_eventloop_lag_ms Prometheus gauge (no config key) |
kubernetes | Kubernetes CRD config provider (--kubernetes-namespace) |
Bundles: standard is jwt + consumers + forward-auth + cache + acme on top of the
default set; full is every feature above plus standard. static-server and gateway are
shorthands for --no-default-features builds: static serving without the proxy, or the proxy, auth,
cache and ACME stack without static files. proxy, compression, static and hotreload are on
by default.
conduit [-c FILE] start the server (default config: conduit.json, conduit.yaml or conduit.yml)
conduit --kubernetes-namespace NS start from ConduitSite resources instead of a file ('*' = all namespaces; needs kubernetes)
conduit validate [-c FILE] validate the config — exit 0 = ok, exit 1 = errors
conduit features [-c FILE] [--json] which Cargo features this config needs
conduit fmt [-c FILE] [--write] pretty-print / normalise the config
conduit init [--yes] [-o FILE] setup wizard (--yes = non-interactive)
conduit probe [-c FILE] ping every configured upstream and show latency
conduit reload [--admin ADDR] hot-reload the config
conduit status [--admin ADDR] version, uptime, in-flight requests
conduit status [--admin ADDR] --upstream upstream health table (latency, ejected, 5xx)
conduit shutdown [--admin ADDR] graceful shutdown
conduit upstreams [--admin ADDR] list upstream health and latency
conduit upstreams add --route PATH --target URL [--weight N]
conduit upstreams remove --route PATH --target URL
conduit upstreams weight --route PATH --target URL --weight N
conduit completions bash|zsh|fish|power-shell|elvish shell completion script
conduit man generate a man page (roff)
reload, status, shutdown and upstreams talk to the Admin API. Changes made
with upstreams add|remove|weight live in memory only and are dropped by the next reload.
Full flag reference and exit codes: docs/cli.md
Config is a single YAML or JSON file. Without -c, Conduit looks for conduit.json, then
conduit.yaml, then conduit.yml in the current directory.
String values can reference environment variables ("$VAR"), which are expanded at start-up and
on reload. Keep secrets there rather than in the file.
# conduit.yaml — annotated overview of common fields
port: 443
host: example.com # virtual host (omit for catch-all)
tls:
acme: # automatic certificates (--features acme)
email: [email protected]
http2: {} # HTTP/2 settings (on TLS ports it is negotiated via ALPN anyway)
compression: true
securityHeaders: true
proxy:
/api:
targets:
- http://api-1:4000
- http://api-2:4000
strategy: least-conn
healthCheck: { path: /health }
retry: { attempts: 2, conditions: [5xx, connection_error] }
cache: { store: memory, ttlSecs: 60 }
rateLimit:
windowSecs: 60
limit: 300
jwtAuth: # --features jwt
jwksUrl: https://auth.example.com/.well-known/jwks.json
audience: [my-api]
logging: json
metrics: { path: /__metrics__, token: "$METRICS_TOKEN" }
healthCheck: true
→ All fields with examples: docs/configuration.md
→ Ready-to-run configs: examples/
Changing the config does not mean restarting the server: edit the file, run conduit validate, then
conduit reload. Most settings apply live, without dropping connections. Settings that need a
restart (the port, certificate files, global.workers, global.admin.bind,
global.shutdownTimeoutSecs) are refused by reload with a 400 that names the field.
An optional management server, off unless you configure global.admin.bind. Bind it to loopback
(127.0.0.1:2019, which is also where the CLI commands look by default). Binding it to another
interface is possible, but only do that behind a VPN or an SSH tunnel and always with a token.
# conduit.yaml — `global` sits next to `sites:`; in a single-site config it is ignored
global:
admin:
bind: "127.0.0.1:2019"
token: "$ADMIN_TOKEN" # Bearer token, strongly recommended
sites:
- port: 8080
proxy: http://localhost:4000
conduit reload # hot-reload the config
conduit status # uptime, version, in-flight count
conduit status --upstream # upstream health table
conduit upstreams add --route /api --target http://new-backend:4000
# or call the API directly (with a token configured, add -H "Authorization: Bearer $ADMIN_TOKEN")
curl http://localhost:2019/status
curl -X POST http://localhost:2019/reload
curl -X DELETE "http://localhost:2019/cache/purge?url=https://example.com/api/data"
curl -X POST http://localhost:2019/ip-deny -d '{"cidr":"1.2.3.0/24"}'
→ All endpoints with request and response examples: docs/admin.md
Every pull request runs a Performance report in CI that measures reverse-proxy passthrough
for the PR head and for its base commit, back to back on the same runner, and posts the
difference. It is a trend signal that catches regressions, not a lab measurement, so this README
quotes no throughput figure: GitHub-hosted runners land on different CPUs from one run to the
next, and throughput depends on the hardware and on global.workers. If you quote a number,
quote its setup too. This is the setup CI uses:
| Runner | GitHub-hosted ubuntu-latest: 4 vCPU, 16 GB RAM. The host CPU model differs between runs and is printed in every report. |
| Conduit | cargo build --release with default features (lto = true, codegen-units = 1, stripped), run with the default of one worker thread (global.workers unset) |
| Upstream | a small Go net/http server on port 4000 returning a fixed 22-byte JSON body (source) |
| Load generator | oha 1.16.0: oha -z 10s -c 50 --no-tui --output-format json http://127.0.0.1:8080/ |
| What is compared | PR head against PR base, built and measured in the same job |
The server configuration, as JSON (what CI writes) and as the equivalent YAML:
{ "port": 8080, "proxy": "http://127.0.0.1:4000" }
port: 8080
proxy: http://127.0.0.1:4000
Throughput grows with global.workers, up to what the machine can give. For scripts that
measure requests per second and CPU cost per request on your own hardware, see
scripts/bench/; docs/benchmarks.md has the
step-by-step reproduction, the reason CI uses one worker, and how to compare Conduit with another
proxy fairly.
tls.versions and tls.ciphers are
rejected by conduit validate because the TLS layer offers no way to apply them.port, tls.cert / tls.key, workers,
global.shutdownTimeoutSecs and global.admin.bind. Everything else reloads in place.global.workers to use more cores.Conduit ships a JSON Schema for completion, hover docs and inline validation in JSON and YAML configs.
VS Code — JSON: add one line to your config:
{
"$schema": "https://raw.githubusercontent.com/lopatnov/conduit/main/schema/conduit.schema.json",
"port": 3000
}
VS Code — JSON: workspace-wide (all conduit*.json files):
// .vscode/settings.json
{
"json.schemas": [{
"fileMatch": ["conduit.json", "conduit.*.json"],
"url": "https://raw.githubusercontent.com/lopatnov/conduit/main/schema/conduit.schema.json"
}]
}
VS Code — YAML: add to .vscode/settings.json (requires the
YAML extension):
// .vscode/settings.json
{
"yaml.schemas": {
"https://raw.githubusercontent.com/lopatnov/conduit/main/schema/conduit.schema.json":
["conduit.yaml", "conduit.yml", "conduit.*.yaml"]
}
}
IntelliJ / WebStorm: Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema
Mappings → add the URL https://raw.githubusercontent.com/lopatnov/conduit/main/schema/conduit.schema.json
with the file pattern conduit*.json, conduit*.yaml.
Contributions are welcome. Read CONTRIBUTING.md before opening a PR.
Apache 2.0 © 2024–2026 Oleksandr Lopatnov
| Static files | Static files · Fallback |
| TCP and uploads | TCP proxy · Upload |
| Response shaping | Compression · Security headers · CORS · Error masking · Response transform |
| TLS | TLS / HTTPS · mTLS · HTTP/2 |
| Authentication | Basic · API key · JWT · Forward auth · Consumers |
| Traffic control | Rate limiting · Limits · Priority routing · IP filter |
| Scripting | Rhai guide · WebAssembly guide |
| Observability | Logging · Metrics · OpenTelemetry · Response time · Server-Timing |
| Operations | Hot reload · Admin API |
| Developer tools | CLI · JSON Schema for editors |
| Packaging and Kubernetes | Deployment guide · ConduitSite resources · Building |