Enterprise-grade open-source forward proxy
HTTP · HTTPS · SOCKS5 · WebSocket
Single binary · Zero dependencies · Written in Go
Most forward proxies force you to choose: commercial appliance with vendor lock-in, or a minimal open-source tool you have to build around. Culvert is neither. It ships as a single Go binary with everything built in - SSL inspection, identity-aware policy, antivirus scanning, threat feeds, a full admin UI, and enterprise auth (OIDC, SAML, LDAP) - all without plugins, agents, or runtime dependencies.
Deploy it with docker-compose up -d and you get a production-ready proxy with zero-trust default-deny, real-time dashboards, Prometheus metrics, and a 10-check CI security pipeline. Scale it out with the built-in gRPC Control Plane for multi-node deployments. Extend it with the plugin API when you need custom logic.
- HTTP / HTTPS forward proxy with full CONNECT tunnel support
- SSL/TLS inspection - on-the-fly MITM certs (ECDSA P-256), per-host bypass, LRU cert cache (10k entries, 1h TTL)
- SOCKS5 proxy (RFC 1928/1929) with username/password auth
- WebSocket tunneling through CONNECT
- PAC file auto-generation for browser auto-configuration
- Upstream proxy chaining with round-robin, health checks, and automatic failover
- Circuit breaker - stops forwarding to hung upstream proxies after consecutive failures
- Default deny - unmatched traffic is blocked
- Priority-based rules - first match wins across 8 condition types:
- Source IP / CIDR
- Authenticated identity
- IdP group membership
- Auth source (OIDC, SAML, LDAP, local)
- Destination FQDN (exact + wildcard)
- URL category (Social, Streaming, Gambling, News, Malicious, Adult, ...)
- Destination country (GeoIP, fail-closed on cache miss)
- Time schedule (day of week + time window + timezone)
- Actions: Allow, Drop, Block Page, Redirect
- Per-rule SSL action: Inspect (full MITM) or Bypass (transparent tunnel)
- Policy conflict detection - warns on overlapping rules at same priority
- Per-rule Prometheus metrics with cardinality cap
- Local auth with bcrypt hashes and first-run setup wizard
- OIDC Authorization Code + PKCE (Okta, Azure AD, Google, Auth0, Keycloak)
- SAML 2.0 SP-initiated SSO (Okta, Azure AD, ADFS)
- LDAP bind + search with group resolution (Active Directory, OpenLDAP, FreeIPA)
- Multi-IdP - simultaneous providers with email-domain routing
- TOTP 2FA (RFC 6238, inline stdlib implementation) with backup codes for admin accounts
- RBAC - admin / operator / viewer roles
- ClamAV antivirus (INSTREAM protocol, concurrency-limited scanning, auto-detection)
- YARA rules - pure-Go engine (no libyara), runtime reload, ReDoS-safe (5s timeout)
- Threat feeds - URLhaus + OpenPhish with hourly sync, dynamic domain allowlist for hosting platforms (GitHub, Google Drive, etc.)
- DPI - regex content scanning on decrypted HTTPS responses
- File-type blocking - 5 named profiles (Executables, Archives, Documents, Media, Strict)
- Domain blocklist with wildcard matching and allow-list mode
- URL category database (UT1) with background sync
- SHA-256 scan cache with configurable size + TTL
- Remote scan sidecar - process-isolated ClamAV/YARA scanning via remote microservice
- Blocklist feed syncer - auto-sync domain blocklists from remote URLs
Single-page application with real-time updates:
| Panel | Description |
|---|---|
| Dashboard | Live stats, timeseries chart, top domains, country traffic map |
| Live Feed | Real-time request log with per-status badges, filtering, CSV/JSON export |
| Blocklist | Domain entries with wildcards; allow-list / deny-list toggle |
| Policy | Visual PBAC rule editor with all 8 condition types |
| Policy Tester | Dry-run evaluation against any host/user/IP |
| Security | IP filter, rate limiting, connection limits, block page editor, scanner status |
| File Block | File-type blocking profile selector |
| Rewrite | Per-host header rewrite rules (request + response) |
| Upstream Proxies | Parent proxy chaining with health checks and circuit breaker |
| IdP Providers | Step-by-step wizard for OIDC, SAML, LDAP |
| SSL / TLS | Root CA viewer, custom TLS upload, SSL bypass patterns |
| CA Management | CA lifecycle, PEM download, cache stats, OCSP toggle, force rotation |
| Cluster Nodes | Multi-node role display, DP node metrics, enrollment command generator |
| Node Groups | Label-based node grouping with geo-aware auto-labeling |
| Bandwidth / QoS | Per-group bandwidth policies with token bucket rate limiting |
| Config Versions | Auto-snapshot history, side-by-side diff, one-click rollback |
| PAC | PAC file generator with custom exclusions |
| Audit Log | Tamper-evident JSONL trail of all admin actions |
| Diagnostics | Operator contract — storage, policy load, root CA, session HMAC, CDR, cluster TLS posture, release-management health, config-version health, risky-but-allowed warnings |
| Governance | Read-only control-plane visibility — route inventory, C2/C2c/C4 counters, derived health, parity-test catalog (admin-only) |
| Users | User management with RBAC role assignment |
| Settings | Session timeout, UI access control, syslog, config export/import |
- Prometheus metrics - requests, blocks, auth failures, AV scans, YARA matches, bytes transferred, latency histogram, per-rule counters
- Real-time SSE dashboard feed
- Structured logging - text or JSON with
req_id,identity,rule,actionfields - Rotating log files with configurable size threshold
- Syslog forwarding (UDP/TCP, RFC 3164 + RFC 5424) for Splunk, Elastic, QRadar
- JSONL audit trail with actor identity enrichment
- Webhook alerts - HMAC-SHA256 signed notifications for threats, blocks, lockouts
- Request tracing - auto-generated X-Request-ID for end-to-end correlation
- Release catalog - catalog loader with fail-closed validation, manifest hash binding, ed25519 signature verification (enforce-by-default when trust roots are present), freshness (
expires_at) and rollback (catalog_version) protection, and channel pointers for recommended/LTS/critical releases - Pinned-image dispatch - Control Plane dispatches only immutable
repo@sha256:<digest>image refs; mutable tags are refused before the maintenance agent is called - Maintenance-agent flow - release dispatch uses the agent's existing
/v1/status,/v1/upgrades/apply, and/v1/operations/{op_id}endpoints, then verifies success fromrunning_image.repo_digests - Resume API - accepted operations expose their op id, idempotency key, and target digest for explicit status/resume calls while the Control Plane process is running
- Single-flight guard - one active release dispatch per agent; concurrent requests are rejected instead of queued or duplicated
- Digest verification gate - an agent-reported success is not trusted until the running digest matches the catalog-pinned digest
- Legacy Docker updater retained - the Docker update sidecar is still present for compatibility while the catalog + dispatch + maintenance-agent path is proven in production
- Local repository allowlist - dispatch is limited to the configured release repository; publish catalogs against that same repository until repo-rewrite wiring is enabled
- Hot config reload -
SIGHUPreloads blocklist, policy, rewrite, rate limit, upstream pool - Graceful shutdown - lifecycle context + 15s drain window for active tunnels
- CA auto-rotation - daily expiry check, auto-rotates 30 days before expiry
- OCSP/CRL checking on upstream TLS certificates (fail-closed)
- Per-IP connection limiting (configurable, default 1024) with runtime admin API
- Brute-force lockout - 5 failures triggers 15-min IP + user lock
- Admin API rate limiting - 60 req/min per IP on mutating endpoints
- Atomic file writes for CA bundle and config persistence
- PBKDF2 600k iterations (NIST SP 800-132 2024) for CA key encryption
- Post-Quantum Cryptography - ML-KEM-768 hybrid key exchange (Go 1.25), protects against "Harvest Now, Decrypt Later" attacks
- Password complexity - enforces 8+ characters, mixed case, digit requirement
- Log levels - runtime DEBUG/INFO/WARN/ERROR with admin API control
- Control Plane / Data Plane - gRPC config sync with mTLS, per-node metrics aggregation
- Cluster dashboard - connected node list, health, request counts, enrollment wizard
- Node groups - label-based selectors with auto GeoIP labeling on enrollment
- Bandwidth / QoS - per-group token bucket rate limiting with admin UI
- Config versioning - automatic snapshots on every mutation, side-by-side diff, one-click rollback (50 versions)
- Rolling upgrades - orchestrated cluster updates with drain, canary, HA sync
- PAC / threat feed / secrets sync - full config snapshot pushed to data plane nodes
- Bootstrap generator - one-line curl|bash enrollment scripts and docker-compose templates for DP nodes
- Exponential backoff on connection failures (2s–60s)
- Client mTLS for upstream proxy authentication
- See Deployment Guide for single-node, multi-node, and upstream chaining setup
- Plugin API -
Middlewareinterface for custom request/response inspection - HSM/KMS integration -
KeyProviderinterface (AWS KMS, Azure Key Vault, PKCS#11)
Culvert is lightweight by design - a single Go binary with no runtime dependencies. Resource requirements scale with traffic volume, SSL inspection depth, and whether antivirus scanning is enabled.
All benchmarks below are based on x86_64 (AMD64) processors. ARM64 (AWS Graviton, Apple Silicon) typically achieves 10-20% better throughput per vCPU due to Go's efficient ARM code generation. If deploying on ARM, you can conservatively use the same numbers or reduce vCPU count by one tier.
| Profile | Users | Requests/sec | Max throughput | Concurrent conns | SSL Inspection | ClamAV | YARA |
|---|---|---|---|---|---|---|---|
| Home Lab | 1-5 | < 50 | ~100 Mbps | < 100 | Optional | Off | Off |
| Small Office | 10-50 | 50-500 | ~500 Mbps | 100-1000 | Recommended | Optional | Optional |
| Enterprise (single node) | 100-500 | 500-2000 | ~1 Gbps | 1000-5000 | Yes | Yes | Yes |
| Enterprise (cluster) | 500-5000+ | 2000-10000+ | ~10 Gbps (multi-DP) | 5000+ | Yes | Yes | Yes |
When to cluster: A single node with SSL inspection + ClamAV handles ~500 concurrent users comfortably. Beyond 500 users or 1 Gbps sustained throughput, deploy a Control Plane + Data Plane cluster and add DP nodes horizontally.
| Resource | Home Lab | Small Office | Enterprise |
|---|---|---|---|
| CPU | 1 vCPU | 2 vCPU | 4+ vCPU |
| RAM (proxy only) | 128 MB | 256 MB | 512 MB - 1 GB |
| RAM (with ClamAV) | 512 MB | 1 GB | 2 GB |
| RAM per connection | ~10 KB idle / ~138 KB active | same | same |
| Storage (base) | 50 MB (binary + config) | 50 MB | 50 MB |
| Storage (ClamAV DB) | - | 300 MB | 300 MB |
| Storage (GeoIP DB) | 5 MB | 5 MB | 5 MB |
| Storage (UT1 category feed) | 150 MB | 150 MB | 150 MB |
| Storage (logs) | 100 MB - 1 GB | 1 - 5 GB | 5 - 50 GB |
| Storage (admin settings) | < 10 KB | < 10 KB | < 10 KB |
| Disk type | Any (HDD OK) | SSD recommended | SSD required |
Each proxied connection consumes memory proportional to its state:
| State | RAM per connection | Notes |
|---|---|---|
| Idle (keepalive) | ~10 KB | Go net/http conn + buffers |
| Active HTTP forward | ~70 KB | Request/response buffers (32 KB each) |
| SSL-inspected tunnel | ~138 KB | 2x 32 KB relay buffers (pooled) + TLS state |
| Body scanning (ClamAV/YARA) | +64 KB - 4 MB | Buffered body up to scan limit, then freed |
| WebSocket relay | ~138 KB | Same as tunnel (bidirectional relay) |
At 5000 concurrent SSL-inspected connections: ~670 MB for connection buffers alone. Plan RAM accordingly for high-concurrency environments.
- ClamAV is the largest resource consumer. It downloads ~300 MB of virus signatures on first boot and keeps them in memory (~500 MB RSS). If you don't need antivirus scanning, disable it to save ~500 MB RAM. ClamAV signature reloads (freshclam updates, typically every 4 hours) cause a brief CPU spike (~2-5 seconds) but do not block proxy traffic - the reload happens in the ClamAV sidecar process, not in Culvert itself. No over-provisioning needed.
- SSL inspection adds ~1 KB RAM per cached leaf certificate (LRU cache, 10K max = ~10 MB). CPU impact is ~0.5ms per new TLS handshake using ECDSA P-256 signing (Culvert's default). RSA 2048 would be ~5x slower per signing operation, but Culvert exclusively uses ECDSA P-256 for its internal CA - no RSA path exists. Cached certificates (cache hit) have zero signing overhead.
- Log rotation is automatic. Request logs rotate at 100 MB (configurable via
-request-log-max-mb), audit logs at 50 MB, system logs at 50 MB. Storage I/O: log writes are append-only and sequential - HDD is adequate for Home Lab. For Small Office+ with SSL inspection (high request volume), SSD is recommended to avoid I/O wait impacting proxy latency. - Threat feeds (URLhaus + OpenPhish) add ~5-20 MB RAM depending on feed size.
- UT1 category feed (University of Toulouse) downloads a ~50 MB tarball on first sync, stored in BadgerDB at ~150 MB on disk + ~50-100 MB RAM for the index. Provides millions of categorized domains (Adult, Gambling, Malicious, etc.) auto-synced hourly. The SaaS category feed (AI, Marketing, etc.) adds negligible storage (< 100 KB JSON).
- Prometheus metrics are stateless - scraped externally, no local storage.
- Post-quantum (ML-KEM-768) adds ~1 KB to initial TLS handshakes. No ongoing RAM/CPU impact.
- Docker image size: ~30 MB (distroless base + static Go binary).
For a functional deployment with all features enabled:
2 vCPU | 1.5 GB RAM | 2.5 GB disk (SSD recommended)
For proxy-only (no AV, no SSL inspection):
1 vCPU | 128 MB RAM | 100 MB disk (any)
| Component | CPU | RAM | Storage | Notes |
|---|---|---|---|---|
| Control Plane | 2 vCPU | 512 MB | 500 MB | No proxy traffic - config sync, enrollment, dashboard only |
| Control Plane (HA pair) | 2 vCPU each | 512 MB each | 500 MB each | Leader + standby with automatic failover |
| Data Plane node | 2 vCPU | 1 GB | 1 GB | Handles proxy traffic, receives config from CP |
| Data Plane + ClamAV | 2 vCPU | 2 GB | 1.5 GB | Add ~1 GB RAM + 300 MB disk for AV |
Scale reference:
| Setup | Nodes | Total resources | Throughput | Handles |
|---|---|---|---|---|
| Small cluster | 1 CP + 2 DP | 6 vCPU, 2.5 GB RAM | ~2 Gbps | ~1000 concurrent users |
| Medium cluster | 1 CP (HA) + 5 DP | 12 vCPU, 6 GB RAM | ~5 Gbps | ~5000 concurrent users |
| Large cluster | 1 CP (HA) + 10 DP | 22 vCPU, 11 GB RAM | ~10 Gbps | ~10000+ concurrent users |
- CP is lightweight - it only serves gRPC config sync, node enrollment, and the admin dashboard. No proxy traffic flows through it.
- DP nodes are stateless - they receive their entire config from the CP on connect. Lose a DP, spin up a new one, it auto-enrolls and gets the full config in seconds.
- Bandwidth/QoS policies are enforced per-DP, so rate limits scale linearly with node count.
- Network: CP to DP communication uses gRPC over mTLS. Typical bandwidth: < 1 KB/s per node (config sync + heartbeat every 30s).
- Horizontal scaling: each DP node adds ~1 Gbps throughput capacity (with SSL inspection). Scale is linear - no shared state between DP nodes.
Works on Ubuntu, Debian, RHEL, CentOS, Rocky, Alma, Fedora, Amazon Linux, and Arch. Runs anywhere Linux runs: AWS EC2, Azure VM, GCP Compute, DigitalOcean, Hetzner, bare metal. Installs Docker, clones the repo, pulls the pre-built images and starts everything:
curl -fsSL https://raw.githubusercontent.com/KidCarmi/Culvert/main/scripts/install.sh | bashThe script handles all Docker installation quirks (snap removal, compose v2, distro packages) so you don't have to.
git clone https://github.com/KidCarmi/Culvert
cd Culvert
docker build -t culvert/proxy:pinned . # seed the local-only image tag the compose file resolves
docker compose up -dNo configuration required - the setup wizard creates your admin account on first visit.
pull access denied for culvert/proxy? The compose file resolves the local-only tagculvert/proxy:pinned(a security measure — the proxy image is pinned at the sudo boundary, never pulled by name from a registry). Seed it with thedocker buildline above, or:docker pull ghcr.io/kidcarmi/culvert:latest && docker tag ghcr.io/kidcarmi/culvert:latest culvert/proxy:pinned
| Endpoint | URL | Notes |
|---|---|---|
| HTTP/HTTPS Proxy | http://localhost:8080 |
Configure browser/PAC to point here |
| SOCKS5 Proxy | socks5://localhost:1080 |
Disabled by default, enable via config |
| PAC File | http://localhost:8080/proxy.pac |
Auto-config for browsers |
| Admin Web UI | https://localhost:9090 |
Accept the self-signed cert on first visit |
| Health Check | http://localhost:8080/health |
{"status":"ok",…} |
| Prometheus Metrics | http://localhost:8080/metrics |
Optional bearer token protection |
# Verify it works
curl http://localhost:8080/health
curl -x http://localhost:8080 https://example.comThree things to do once the containers are up:
- Setup wizard — open
https://<host>:9090in a browser, accept the self-signed cert, and create the first admin account. This step is required before any other API call. - Verify readiness — confirm Culvert is fit to take traffic:
Returns
curl http://<host>:8080/ready
200with{"status":"ready", "checks":{...}}when ready,503with"status":"not_ready"when a gating check fails. Seedocs/OPERATIONS.mdfor the full checks-map reference. - Open Diagnostics — Admin UI → Infrastructure → Diagnostics. The page surfaces the operator contract: storage path, policy load, root CA, session HMAC, CDR, cluster TLS posture, release-management health, config-version health, and any active risky-but-allowed warnings (
cluster-insecure, unauth mode). Resolve anyfailrows before exposing the proxy to clients.
The catalog-driven release path is being rolled out as the future default (the legacy Docker updater remains the supported fallback until at least one production catalog-driven update succeeds — see docs/operator/enterprise-release-catalog-plan.md). The Control Plane loads a trusted release catalog, resolves the requested release or channel to an immutable repo@sha256:<digest> image ref, dispatches that ref to the local maintenance agent, polls the operation, and verifies the final running digest before reporting success.
The quick-start installer attempts to wire the local maintenance agent automatically over its Unix-domain socket. It never mounts /var/run/docker.sock into the proxy. If Docker is rootless, userns-remapped, customized, or the validation checks fail, the installer leaves Release Management disabled and prints the custom-wiring path instead.
Operator-facing API flow:
curl -k https://<host>:9090/api/releases
curl -k "https://<host>:9090/api/releases/current?agent=local"
curl -k -X POST https://<host>:9090/api/releases/dispatch \
-H 'Content-Type: application/json' \
-d '{"agent":"local","channel":"recommended"}'
curl -k "https://<host>:9090/api/releases/dispatch/status?agent=local"The catalog lives under the configured data directory at release_catalog/. Signature verification is enforce-by-default: whenever a trust root is present (a baked-in official key, or operator keys via CULVERT_RELEASE_CATALOG_TRUST_KEYS), catalogs must be validly signed, unexpired (expires_at), and at or above the highest accepted catalog_version (rollback protection). With no trust roots configured, Release Management stays disabled and /api/releases reports available:false — an unsigned catalog is never auto-trusted. Configure only public trust roots in CULVERT_RELEASE_CATALOG_TRUST_KEYS; never put private signing keys or registry credentials there. CULVERT_RELEASE_CATALOG_VERIFY=permissive|disabled is a deliberate, logged break-glass override (e.g. local dev with an unsigned catalog) — leave it unset in production. Optionally set CULVERT_RELEASE_CATALOG_URL to a signed-catalog origin: when it is set and trust roots are present, the Control Plane fetches and verifies that catalog at startup (signature + freshness + rollback) and atomically installs it into release_catalog/, so a clean install reaches available:true with no manual file placement — anything wrong leaves the existing catalog untouched and reports available:false. Verification happens in the binary; the installer never bakes a default URL, and auto-seed runs only in enforce mode (an unsigned catalog is never auto-downloaded). The legacy Docker updater remains available for compatibility, but release catalog dispatch does not call its APIs or Docker socket path.
Backup & restore. For the supported Docker Compose backup, restore, and cleanup commands, see
docs/operator/docker-compose-backup-restore.md.
cp config.example.yaml config.yaml # edit as needed
# Uncomment the config.yaml volume mount in docker-compose.yml, then:
docker compose up -ddocker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d
# Grafana → http://localhost:3000 (admin / culvert)The Culvert Overview dashboard (12 panels: traffic, latency, security blocks, top policy rules) is auto-provisioned from deploy/grafana/dashboards/culvert-overview.json - no manual import needed.
# Download the latest release, then:
./culvert # proxy :8080, admin UI :9090
./culvert -port 3128 -socks5-port 1080
./culvert -config config.yamlcurl -x http://localhost:8080 https://example.com
# With credentials
curl -x http://alice:secret@localhost:8080 https://example.com
# Via environment variable
export http_proxy=http://localhost:8080
export https_proxy=http://localhost:8080
# PAC file (auto-configure browsers)
# Point your browser to: http://localhost:8080/proxy.paccurl --proxy socks5://localhost:1080 https://example.com
# SSH tunneling through SOCKS5
ssh -o ProxyCommand="nc -X 5 -x localhost:1080 %h %p" user@remoteWhen SSL inspection is enabled, import the Root CA into your browser/OS trust store:
# Download CA from Admin UI → Certificates, or:
curl -k https://localhost:9090/api/ca-cert > culvert-ca.crt
# Linux
sudo cp culvert-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
# macOS
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain culvert-ca.crt
# Windows (PowerShell as Admin)
Import-Certificate -FilePath culvert-ca.crt -CertStoreLocation Cert:\LocalMachine\RootCulvert works out of the box with sensible defaults. All settings can be managed through the Admin Web UI, a YAML config file, CLI flags, or a combination. See config.example.yaml for full documentation of all 70+ fields.
proxy:
port: 8080
ui_port: 9090
socks5_port: 1080 # 0 = disabled
default_action: block # allow | block | drop
auth:
ldap:
url: ldaps://ldap.corp.com:636
bind_dn: "cn=svc-culvert,ou=Services,dc=corp,dc=com"
base_dn: "ou=Users,dc=corp,dc=com"
required_group: "proxy-users"
security:
ip_filter_mode: allow # allow | block | "" (off)
ip_list:
- 192.168.1.0/24
rate_limit: 60 # requests/min per IP
max_conns_per_ip: 256
upstream:
proxies:
- url: http://parent-proxy:3128
health_interval: 30s
circuit_breaker:
threshold: 5
timeout: 30s
rewrite:
- host: "*.internal.example.com"
req_set:
X-Forwarded-By: Culvert
resp_remove:
- Server
- X-Powered-ByCore:
-port int Proxy listening port (default 8080)
-ui-port int Admin Web UI port (default 9090)
-socks5-port int SOCKS5 proxy port (0 = disabled)
-config string Path to config.yaml
TLS:
-ca-path string Root CA bundle persistence path (/data/ca.bundle)
-tls-cert string Custom TLS certificate for Web UI
-tls-key string Custom TLS key for Web UI
-ui-no-tls Serve Web UI over plain HTTP (not recommended)
Auth & Access:
-ui-users-file string Persistent admin user database (/data/ui_users.json)
-ui-allow-ip string Comma-separated CIDRs allowed to access the Web UI
-session-timeout int Admin session lifetime in hours (default 8)
Rules & Filtering:
-blocklist string Domain/IP blocklist file path
-policy string Policy rules JSON file path
-geoip-db string MaxMind GeoLite2-Country.mmdb path
Security Scanning:
-clamav-addr string ClamAV address - tcp:host:port or unix:/path/to/clamd.sock
-yara-rules-dir string Directory containing .yar / .yara rule files
-threat-feed-db string Threat feed local database path
Logging:
-logfile string Request log file (rotated at -log-max-mb)
-log-max-mb int Log rotation threshold in MB (default 50)
-audit-log string Persistent JSONL audit log path
-syslog string Remote syslog - udp://host:514 or tcp://host:601
Metrics:
-metrics-token string Bearer token protecting /metrics (empty = open)
-rate-limit int Max requests/min per source IP (0 = off)
Distributed (Control Plane):
-cp-grpc-addr string Control Plane gRPC listen address (e.g. :50051)
-cp-grpc-cert string Control Plane gRPC TLS certificate
-cp-grpc-key string Control Plane gRPC TLS key
-cp-grpc-ca string Control Plane gRPC CA for mTLS client validation
Distributed (Data Plane):
-dp-cp-addr string ControlPlane gRPC address to connect to (e.g. cp.corp:50051)
-dp-node-id string Unique node identifier (default: hostname)
-dp-cert string Data Plane gRPC client TLS certificate
-dp-key string Data Plane gRPC client TLS key
-dp-ca string Data Plane gRPC CA certificate
See docs/deployment-guide.md for complete single-node, multi-node, and upstream chaining examples.
| Variable | Description |
|---|---|
CULVERT_CA_PASSPHRASE |
CA private key encryption passphrase (required for SSL inspection) |
CULVERT_C2_ENFORCE |
C2 metadata-driven admin RBAC enforcement mode. Default = enforce (fail-closed). Set to false/0/no/off to revert to shadow (log-only) mode without rebuild. Read once at startup. |
CULVERT_RELEASE_PROXY_REPO |
Bare image repository allowed for release dispatch. Defaults to ghcr.io/kidcarmi/culvert; do not include a tag or digest. |
CULVERT_MAINT_AGENT_URL |
Local maintenance-agent endpoint for release dispatch. Defaults to the Unix socket path /run/culvert-maint/culvert-maint.sock; http:// and https:// endpoints are also supported for controlled deployments. |
CULVERT_RELEASE_CATALOG_TRUST_KEYS |
JSON array of public Ed25519 catalog trust roots. Public keys only; never place private signing keys or credentials here. |
Available at GET http://localhost:8080/metrics:
| Metric | Type | Description |
|---|---|---|
culvert_requests_total |
counter | All proxy requests |
culvert_requests_allowed |
counter | Forwarded requests |
culvert_requests_blocked |
counter | Blocked requests (all reasons) |
culvert_requests_auth_fail |
counter | Authentication failures |
culvert_bytes_sent_total |
counter | Total bytes sent to clients |
culvert_bytes_recv_total |
counter | Total bytes received from upstream |
culvert_request_duration_seconds |
histogram | Request latency (11 buckets, 5ms–10s) |
culvert_rule_hits_total{rule="..."} |
counter | Per-rule hit counter |
culvert_av_scans_total |
counter | ClamAV scans performed |
culvert_av_detections_total |
counter | Malware detections |
culvert_yara_matches_total |
counter | YARA rule matches |
culvert_threat_feed_blocks_total |
counter | Threat feed blocks |
culvert_blocklist_size |
gauge | Blocklist entry count |
culvert_policy_rules |
gauge | Active PBAC rule count |
culvert_uptime_seconds |
gauge | Proxy uptime |
Implement the Middleware interface to add custom request inspection:
package main
import "net/http"
type MyPlugin struct{}
func (p *MyPlugin) Name() string { return "my-plugin" }
func (p *MyPlugin) OnRequest(clientIP, method, host string) Decision {
if host == "ads.example.com" {
return DecisionBlock
}
return DecisionAllow
}
func (p *MyPlugin) OnResponse(resp *http.Response) {
resp.Header.Del("Server")
}
func init() { RegisterPlugin(&MyPlugin{}) }Plugins run before every other check and can short-circuit the chain.
Culvert follows a defence-in-depth approach:
| Area | Implementation |
|---|---|
| Zero Trust | Default-deny policy engine; unmatched traffic is blocked |
| SSRF prevention | isPrivateHost() resolves DNS and rejects private/loopback IPs before every outbound dial |
| Log injection (CWE-117) | sanitizeLog() strips \n, \r, \t; %q format verb; X-Request-ID sanitized at source |
| Open redirect | isSafeRedirectURL() validates scheme + non-private host |
| Brute-force | IP + user lockout after 5 failures (15 min cooldown) |
| Admin API rate limiting | 60 req/min per IP on mutating endpoints |
| Slowloris | 60s read deadline on SSL-inspected connections |
| Session security | HMAC-SHA256 signed cookies with per-session 128-bit Jti; dynamic Secure flag; fixation prevention; revocation list with disk persistence and gRPC gossip |
| Admin RBAC defense-in-depth | Metadata-driven C2 enforcement layer in addition to per-handler requireRole; report-only audit-completion (C2c) and role-divergence (C4) detectors; governance health surface at /api/governance/control-plane |
| CA key protection | AES-256-GCM + PBKDF2-SHA256 (600k iterations) at rest |
| OCSP/CRL | Upstream certificate revocation checking (fail-closed) |
| Hop-by-hop | RFC 7230 compliant - parses Connection header for dynamic names |
| GeoIP | Fail-closed on cache miss (unknown country = no match) |
| Header scrubbing | Strips private IPs from X-Forwarded-For, removes X-User-Identity |
| Post-Quantum (PQC) | ML-KEM-768 hybrid key exchange via Go 1.25 - quantum-resistant on all TLS connections |
Culvert is quantum-resistant by default. Go 1.25's crypto/tls automatically negotiates ML-KEM-768 (formerly Kyber) hybrid key exchange on all TLS connections when the peer supports it. This protects against "Harvest Now, Decrypt Later" attacks where an adversary records encrypted traffic today and decrypts it with a future quantum computer.
| Connection | PQC Key Exchange | Notes |
|---|---|---|
| Browser to Admin UI | Auto-negotiated | Chrome 124+, Firefox 128+, Edge 124+ |
| Proxy to upstream servers | Auto-negotiated | When upstream supports ML-KEM |
| Control Plane to Data Plane (gRPC mTLS) | Always active | Both sides run Go 1.25 |
| SSL-inspected client connections | Auto-negotiated | When client browser supports ML-KEM |
What's quantum-resistant today: All key exchanges (TLS handshakes) use hybrid X25519 + ML-KEM-768, meaning traffic confidentiality is protected even against quantum computers.
What's still classical: Certificate signing uses ECDSA P-256. PQC signature algorithms (ML-DSA / Dilithium) are not yet in Go's standard library. Culvert will adopt PQC signing when Go adds native support (expected Go 1.26+). This is a lower risk - signatures prove identity at connection time and cannot be "harvested" for future decryption.
No configuration required - PQC is enabled automatically. No performance impact - the ML-KEM handshake adds ~1ms to the initial TLS connection.
Every push runs a 10-check security gate:
- gosec - Go security linter
- govulncheck - reachable CVE detection
- trivy - filesystem + Docker image vulnerability scan
- gitleaks - secret scanning on PR diffs
- staticcheck - advanced static analysis
- hadolint - Dockerfile best practices
- Race tests -
-raceflag on full test suite - Coverage gate - minimum 55% statement coverage
- License compliance - no GPL/AGPL/LGPL/CPAL dependencies
- SBOM generation - CycloneDX JSON via Syft
Plus: CodeQL semantic SAST, Dependency Obituary dependency health scoring, Cosign keyless signing, and SLSA Level 3 provenance on all releases.
main.go - Entrypoint, CLI flags, graceful shutdown, hot reload (SIGHUP)
proxy.go - HTTP/HTTPS/WebSocket handler, SSL inspection, structured logging
socks5.go - SOCKS5 server (RFC 1928/1929)
policy.go - PBAC engine: rule evaluation, conflict detection, GeoIP fail-closed
session.go - HMAC-SHA256 signed cookies, revocation, dynamic Secure flag
ui.go - Admin Web UI bootstrap; routes are registered via register*Routes helpers and tracked in uiRoutes (see CLAUDE.md for the canonical inventory and C2/C2c/C4 governance machinery)
store.go - Config, blocklist, request log, time-series, audit log
security.go - IP filter, rate limiter, SSRF guard, DNS cache
security_scan.go - ClamAV + YARA + threat feed scan coordinator
clam.go - ClamAV INSTREAM client with connection pooling
yara_scan.go - Pure-Go YARA engine with ReDoS timeout
threatfeed.go - URLhaus + OpenPhish sync, domain allowlist
feedsync.go - UT1 URL category syncer
geoip.go - MaxMind GeoLite2 with background cache
upstream.go - Proxy chaining, failover, circuit breaker, health checks
ocsp.go - OCSP/CRL revocation checking
ca.go - Root CA, MITM certs, AES-GCM encryption, LRU cache, auto-rotation
auth.go - Auth provider interface
auth_ldap.go - LDAP bind + search + group resolution
auth_oidc_flow.go - OIDC Authorization Code + PKCE
auth_saml.go - SAML 2.0 SP-initiated SSO
auth_idp.go - Multi-IdP registry with domain routing
identity.go - Identity model (Sub, Groups, Source, Provider)
totp.go - TOTP 2FA (RFC 6238, stdlib HMAC-SHA1)
lockout.go - Brute-force + API rate limiting
connlimit.go - Per-IP connection limiter, X-Request-ID
metrics.go - Prometheus metrics (per-rule, latency, bytes)
logger.go - Structured text/JSON logging with rotation
syslog.go - RFC 3164/5424 syslog forwarding
alerts.go - HMAC-SHA256 signed webhook alerts
events.go - SSE live dashboard stream
config.go - YAML config loading + validation (goccy/go-yaml)
rewrite.go - Per-host header rewrite engine
fileblock.go - File extension/MIME blocking
pac.go - PAC file generation
blockpage.go - Block page HTML template
hashcache.go - SHA-256 scan cache with TTL
controlplane.go - gRPC Control Plane / Data Plane
plugin.go - Middleware plugin chain
catdb.go - URL category database
release_catalog.go - Release catalog validation, manifest hash binding, and digest indexing
release_wiring.go - Release management startup wiring and local maintenance-agent resolver
release_api.go - /api/releases catalog/current/dispatch/status/resume endpoints
release_dispatch.go - Catalog-pinned release planning and repo rewrite checks
release_dispatch_exec.go - Maintenance-agent apply, op polling, and digest verification
release_dispatch_service.go - Dispatch orchestration, single-flight, audit, alert, and resume
update.go - Self-update system (binary + Docker)
update_cluster.go - Rolling cluster update orchestrator (canary, drain, HA sync)
scan_remote.go - Remote scan sidecar client for process-isolated scanning
blocklist_feed.go - Domain blocklist URL feed syncer
bootstrap.go - Bootstrap script/compose generators for node enrollment
static/ - Embedded SPA (vanilla JS, Chart.js)
updater/ - Legacy Docker update sidecar (retained for compatibility)
scripts/ - Install script (multi-distro), CI runner setup
deploy/ - Prometheus + Grafana stack
yara/ - Starter YARA detection rules
Requires Go 1.25+.
go build -o culvert . # build
go test -v -race ./... # full suite with race detector
go test -coverprofile=cover.out ./... # coverage report
go test -fuzz FuzzIsPrivateHost -fuzztime=30s # fuzz SSRF guard| Target | Coverage |
|---|---|
FuzzIsPrivateHost |
SSRF guard (DNS + private IP) |
FuzzIsSafeRedirectURL |
Open redirect prevention |
FuzzParseClamResponse |
ClamAV response parser |
FuzzNormaliseFeedURL |
Threat feed URL normalisation |
FuzzMatchDest |
Policy destination matching |
FuzzParseYARALiteralString |
YARA rule string parser |
docker build -t culvert:dev .
docker run -p 8080:8080 -p 9090:9090 culvert:dev- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Run tests (
go test -race ./...) - Commit your changes
- Open a Pull Request
All PRs are validated by the full CI pipeline including CodeQL, the security gate, and golangci-lint with 18 linters.
