BurrowGate is a self-hosted reverse proxy and access gateway built with Bun. It protects websites and APIs from bots, scrapers, and malicious requests using route‑level policies, GeoIP network controls, browser challenges, rate limiting, managed request protection (WAF rules), header manipulation, request limits, WebSocket transport controls, static asset caching with purge support, TLS termination, and live traffic monitoring.
- Native HTTP and HTTPS listeners on ports 80 and 443
- Multi-site reverse proxy routing by hostname
- Automatic Let's Encrypt certificates using ACME HTTP-01 or DNS-01 (RFC 2136 dynamic updates)
- Uploaded PEM certificate support
- SNI certificate selection for multiple domains
- Transparent HTTP, HTTPS, WebSocket, and secure WebSocket proxying
- Managed request protection with monitor/block modes, per-route overrides, rule exclusions, and auditable outcomes
- Native TCP and UDP stream proxying, including optional incoming TCP TLS termination
- Scheduled application of listener-affecting Site and Stream changes (hostname, port, forward target, certificate, protocol mode), so an edit doesn't land in the middle of active connections
- Per-site and per-route access policies
- Fixed-window, sliding-window, and token-bucket rate limits
- Safe bounded static-asset caching with per-site/route controls, metrics, and scoped purge
- Pluggable challenge providers and ordered challenge chains
- SHA-256 browser proof of work
- Opaque and revocable visitor sessions
- TOTP and WebAuthn (security key) two-factor authentication for dashboard and access-list accounts, with per-site WebAuthn credential scoping
- IPv4, IPv6, CIDR, ASN, and country pass, bypass, block, and challenge rules
- Site-wide and per-route default IP and country actions for allowlists and blocklists, with route rules taking precedence over the site's
- Signed origin verification headers
- Optional per-site/per-route request and response body capture, bounded by size and content type with a self-expiring window, viewable per request from Recent Traffic
- Optional per-site/per-route request and response header capture, with default redaction of Authorization/Cookie/Set-Cookie plus a configurable extra redaction list
- Per-site/per-route CORS policy, answering cross-origin preflight requests directly ahead of verification and access-list sign-in
- Per-site HSTS (
Strict-Transport-Security) with optional includeSubDomains and preload - Resend any captured Recent Traffic request from the dashboard, with editable headers and body, automatic same-site redirect following, and a full hop-by-hop chain in the result
- Paginated traffic, session, route, rule, and site monitoring
- Separate client-side and upstream bandwidth monitoring with per-site, per-IP, protocol, and country totals
- Stream connection logs, live TCP/UDP peers, GeoIP and ASN enrichment, and bandwidth by IP and incoming port
- Per-site traffic retention
- Country-level GeoIP analytics with an interactive SVG world map, and a "Top ASNs" list by network provider
- Country codes, ASNs, country filters, ASN filters, and country/ASN tooltips in traffic and session tables
- Per-site customizable HTML or JSON error responses
- Multi-origin load balancing with priority failover, round robin, weighted round robin, session affinity, and deterministic IP fallback
- Static file origins: serve a folder directly from disk instead of proxying to a backend, with clean URLs, SPA fallback, and HTTP range support, mixable with proxy origins in the same load-balanced pool
- Optional per-origin mTLS (client certificate) to the origin, generated by BurrowGate for one-click download or uploaded from your own PKI
- Optional per-origin trusted CA / BurrowGate-issued origin server certificate, usable independently of mTLS for origins that can't verify client certificates
- Per-origin health checks, automatic unhealthy-origin removal, and optional 503 maintenance mode
- Unified notification system for sites and Streams: origin health, internet-connectivity, system-resource-threshold, and IP auto-ban webhooks to ntfy, Slack, Discord, or signed generic JSON, with per-event-type subscriptions, durable ordered retries, and a searchable delivery log
- Firewall sync: pushes auto-banned and manually-blocked IPs to a UniFi controller, local nftables, OVH's per-IP edge firewall, or an AWS VPC Network ACL, with a never-ban whitelist, automatic private-range exclusion, and per-provider entry caps with oldest-first eviction
- Per-site customizable HTML challenge pages
- Prometheus and OpenTelemetry Collector export through an OpenMetrics endpoint
- In-dashboard update notifications, checking GitHub Releases hourly for a newer stable version
- SQLite by default with PostgreSQL, MySQL, and MariaDB support
- Docker Compose deployment
Requirements:
- A Linux VPS with Docker and Docker Compose
- Public TCP ports 80 and 443
- A domain pointing to the VPS for trusted TLS certificates
Create a directory for BurrowGate and download only the Compose file:
mkdir burrowgate && cd burrowgate
curl -fsSLO https://raw.githubusercontent.com/Rabbit-Company/BurrowGate/main/docker-compose.ymlAlternatively, create a docker-compose.yml file and copy the following content into it:
services:
burrowgate:
image: rabbitcompany/burrowgate:latest
container_name: burrowgate
restart: unless-stopped
init: true
network_mode: host
env_file:
- path: .env
required: false
cap_add:
- NET_BIND_SERVICE
- NET_ADMIN
volumes:
- ./data:/app/data
healthcheck:
test: ["CMD", "bun", "-e", "const response = await fetch('http://127.0.0.1/_burrowgate/health'); if (!response.ok) process.exit(1)"]
interval: 30s
timeout: 5s
start_period: 20s
retries: 3
geoipupdate:
image: ghcr.io/maxmind/geoipupdate
container_name: geoipupdate
restart: unless-stopped
profiles: ["geoip"]
environment:
GEOIPUPDATE_ACCOUNT_ID: "${MAXMIND_ACCOUNT_ID:-}"
GEOIPUPDATE_LICENSE_KEY: "${MAXMIND_LICENSE_KEY:-}"
GEOIPUPDATE_EDITION_IDS: GeoLite2-Country
GEOIPUPDATE_FREQUENCY: "${GEOIPUPDATE_FREQUENCY:-72}"
volumes:
- ./data/geoip:/usr/share/GeoIPStart BurrowGate:
docker compose up -d
docker compose logs burrowgateBurrowGate generates a dashboard password, encryption key, and temporary self-signed certificate on the first startup. Read the generated dashboard password with:
docker compose exec burrowgate cat /app/data/bootstrap-admin-password.txtOpen the dashboard:
https://SERVER_IP/_burrowgate/admin
The browser will warn about the temporary certificate until a trusted certificate is uploaded or issued.
Create a site from the Sites tab:
Name: Sonarr
Public host: sonarr.example.com
Origin URL: http://10.0.0.20:8989
Point sonarr.example.com to the VPS, open the site's TLS settings, and request a Let's Encrypt certificate.
For a runnable two-site authentication demonstration with a protected frontend, separate API, session assertions, backend introspection, caching, and logout, see examples/cross-site-auth.
The default Compose configuration is production ready:
network_mode: hostruns BurrowGate directly on the host network, so it binds host ports 80 and 443 (or whateverBG_HTTP_PORT/BG_HTTPS_PORTare set to) with no Docker port mapping or NAT in between- origins running on the same host are reachable at
localhost/127.0.0.1with no extra Docker networking configuration - runtime data is stored in the
./datadirectory NET_BIND_SERVICEis added for low-port binding, andNET_ADMINfor the optional local-nftables Firewall Sync provider (seedocs/FIREWALL_SYNC.md) - both are scoped to specific binaries via file capabilities (setcap) in the image rather than left on the container's main process
Host networking only works on Linux Docker hosts (the only supported deployment target) and gives the container full access to the host's network interfaces, so firewall the host as if BurrowGate's configured ports were running outside of Docker.
An .env file is optional. Copy the example file only when overriding defaults:
cp .env.example .env
nano .env
docker compose up -d --build --force-recreatePull the new image and recreate the container:
docker compose pull
docker compose down
docker compose up -dThe default Compose file tracks the latest tag, so docker compose pull always fetches the newest release. Runtime data, certificates, and the encryption key live in ./data and are preserved across upgrades.
To upgrade deliberately instead of always tracking latest, pin the image to a specific version or major line:
services:
burrowgate:
image: rabbitcompany/burrowgate:1.9.0 # exact version
# image: rabbitcompany/burrowgate:1 # latest 1.x releaseCheck the releases page for what changed before upgrading, especially across major versions.
BurrowGate checks GitHub Releases for a newer stable version every hour (BG_UPDATE_CHECK_INTERVAL_HOURS, default 1) and shows an Update available badge next to the version number in the dashboard header when one is found. Clicking it opens the release notes and a link to the release on GitHub - there is no automatic download or restart. Disable the check entirely with BG_UPDATE_CHECK_ENABLED=false, useful for air-gapped deployments. See docs/UPDATE_CHECKS.md.
| Variable | Default | Description |
|---|---|---|
BG_ENV |
production |
Runtime environment |
BG_HOST |
0.0.0.0 |
Listener address |
BG_HTTP_ENABLED |
true |
Enable the HTTP listener |
BG_HTTP_PORT |
80 |
Internal HTTP port |
BG_HTTP_PUBLIC_PORT |
80 |
Public HTTP port used in redirects and ACME validation |
BG_HTTPS_ENABLED |
true |
Enable the HTTPS listener |
BG_HTTPS_PORT |
443 |
Internal HTTPS port |
BG_HTTPS_PUBLIC_PORT |
443 |
Public HTTPS port used in redirects |
BG_TLS_LISTENER_DRAIN_TIMEOUT_MS |
5000 |
Grace period before the previous HTTPS listener is force-closed after a certificate reload |
BG_HTTP3_ENABLED |
false |
(Experimental) Add a UDP HTTP/3 listener next to HTTPS; see docs/TLS.md |
DATABASE_URL |
sqlite://./data/burrowgate.db |
Bun.SQL database URL |
BG_ADMIN_USERNAME |
admin |
Dashboard username |
BG_ADMIN_PASSWORD |
generated | Dashboard password |
BG_COOKIE_SECURE |
auto |
Use secure cookies on HTTPS and ordinary cookies on HTTP |
BG_MASTER_KEY |
generated | Encrypts certificate and ACME private keys |
BG_EVENT_RETENTION_DAYS |
7 |
Default monitoring retention assigned to new sites and streams |
BG_BANDWIDTH_FLUSH_INTERVAL_MS |
10000 |
Interval for flushing aggregated bandwidth counters to the database |
BG_BANDWIDTH_MAX_PENDING_KEYS |
50000 |
Maximum exact in-memory site/IP/minute keys before new IPs use country overflow buckets |
BG_HTTP_CACHE_MAX_ENTRIES |
2048 |
Maximum static-asset cache entries held by one BurrowGate process |
BG_HTTP_CACHE_MAX_BYTES |
268435456 |
Maximum total in-memory static-asset cache size |
BG_HTTP_CACHE_MAX_OBJECT_BYTES |
33554432 |
Instance ceiling for one cacheable response body |
BG_BODY_CAPTURE_MAX_BYTES_CEILING |
1048576 |
Instance ceiling for one captured request or response body |
BG_ACCESS_LOGIN_MAX_FAILURE_KEYS |
50000 |
Maximum access-login failure keys retained in memory |
BG_ACCESS_SESSION_ASSERTION_TTL_SECONDS |
300 |
Lifetime of short-lived assertions used for cross-site session introspection |
BG_MAINTENANCE_INTERVAL_SECONDS |
3600 |
Interval between GeoIP and certificate housekeeping runs |
BG_MAINTENANCE_CLEANUP_INTERVAL_SECONDS |
60 |
Interval between short incremental retention-cleanup runs |
BG_MAINTENANCE_CLEANUP_BATCH_SIZE |
250 |
Maximum rows removed by one cleanup write |
BG_MAINTENANCE_CLEANUP_PAUSE_MS |
25 |
Event-loop pause between cleanup writes |
BG_MAINTENANCE_CLEANUP_TIME_BUDGET_MS |
5000 |
Maximum incremental-cleanup time per maintenance run |
BG_PENDING_CHANGE_POLL_INTERVAL_SECONDS |
15 |
Interval between checks for due scheduled Site/Stream changes |
BG_GEOIP_ENABLED |
true |
Enable country-level GeoIP enrichment |
BG_GEOIP_DATABASE_PATH |
./data/geoip/GeoLite2-Country.mmdb |
Local MaxMind database path |
BG_GEOIP_CACHE_ENTRIES |
4096 |
Maximum GeoIP/ASN reader cache entries |
BG_GEOIP_RETRY_SECONDS |
30 |
Retry interval when a MMDB file is not available yet |
BG_GEOIP_ASN_ENABLED |
same as BG_GEOIP_ENABLED |
Enable ASN enrichment |
BG_GEOIP_ASN_DATABASE_PATH |
./data/geoip/GeoLite2-ASN.mmdb |
Local MaxMind ASN database path |
BG_OPENMETRICS_ENABLED |
false |
Expose /_burrowgate/metrics for Prometheus-compatible scraping |
BG_OPENMETRICS_TOKEN |
empty | Optional bearer token protecting the OpenMetrics endpoint |
BG_UPDATE_CHECK_ENABLED |
true |
Check GitHub Releases for a newer version and show a dashboard badge |
BG_UPDATE_CHECK_INTERVAL_HOURS |
1 |
How often to check for a newer version |
BG_DEFAULT_POW_DIFFICULTY |
18 |
Default SHA-256 challenge difficulty |
BG_WEBSOCKET_ENABLED |
true |
Enable WebSocket proxying |
BG_WEBSOCKET_IDLE_TIMEOUT_SECONDS |
120 |
WebSocket idle timeout from 10 to 960 seconds |
BG_STREAM_IDLE_TIMEOUT_SECONDS |
300 |
Idle timeout for established TCP streams |
BG_STREAM_UDP_PEER_IDLE_TIMEOUT_SECONDS |
60 |
Inactivity interval used to close a synthetic UDP peer session |
BG_STREAM_MAX_BUFFERED_BYTES |
1048576 |
Maximum queued TCP data per proxied connection |
BG_STREAM_MAX_UDP_PEERS |
10000 |
Maximum tracked UDP peers per configured stream |
BG_STREAM_MAX_PENDING_EVENTS |
100000 |
Maximum queued Stream lifecycle events during a database outage |
BG_ACME_DIRECTORY_URL |
Let's Encrypt production | ACME directory URL |
BG_ACME_EMAIL |
empty | Default ACME contact email |
See .env.example for every available setting.
DATABASE_URL=sqlite://./data/burrowgate.db
DATABASE_URL=postgres://user:password@postgres:5432/burrowgate
DATABASE_URL=mysql://user:password@mysql:3306/burrowgateBun.SQL selects the database adapter from the URL.
Each site contains:
- a public hostname and optional port
- an HTTP or HTTPS origin URL
- an enabled state
- a default access mode
- default IP and country actions
- a visitor session lifetime
- a traffic retention period
- a challenge policy
- a signing secret for origin verification headers
- TLS and force-HTTPS settings
- HTML or JSON error-response settings
- origin health-check interval, timeout, thresholds, and failure behavior (webhook notifications are configured separately, on the Notifications dashboard)
- a load-balancing algorithm, sticky affinity behavior, and an origin pool with per-origin priority, weight, drain state, and health-path override
- HTTP request/response header policies and request-size limits, with route-level overrides
The selected site is stored in the dashboard URL. Traffic, sessions, network rules, route policies, and actions are scoped to that site.
Changing the public hostname of a site with an active certificate rebuilds the HTTPS listener; that can be scheduled for a chosen time instead of applying immediately. See docs/SCHEDULED_CHANGES.md.
Editing a site exposes a permanent delete action protected by typed-name confirmation. Deletion removes the site's request and bandwidth history, sessions, access memberships and settings, challenges, route and network policies, origins, health history, notification events and deliveries, ACME challenges, TLS settings, certificate, and certificate events in one transaction. Global access users and ACME accounts are preserved because they may be shared. A site cannot be deleted while its certificate is assigned to a TCP Stream or while certificate issuance is active.
Environment-based site seeding is disabled by default. It can be enabled for automated deployments:
BG_SEED_DEFAULT_SITE=true
BG_DEFAULT_SITE_NAME=Sonarr
BG_DEFAULT_PUBLIC_HOST=sonarr.example.com
BG_DEFAULT_ORIGIN=http://10.0.0.20:8989Environment settings only seed an empty database. Existing sites are managed from the dashboard.
BurrowGate can store an ISO country code and an ASN (with organization name) with each request event, visitor session, and stream event; bandwidth buckets keep the country code only. The dashboard renders an interactive SVG world map for request volume, newly created sessions, and client bandwidth by country, plus a "Top ASNs" list by network provider.
Lookups use local GeoLite2-Country.mmdb and GeoLite2-ASN.mmdb files. BurrowGate reuses one reader per database, keeps a bounded LRU cache for each, and stores only the two-letter country code, the ASN, and its organization name. It does not call an external GeoIP API for each request. Either database can be enabled independently; a missing or disabled ASN database just means ASN rules never match and no ASN data is recorded, without affecting country lookups.
The included optional Compose profile runs MaxMind's official database updater for both databases:
MAXMIND_ACCOUNT_ID=123456
MAXMIND_LICENSE_KEY=replace-with-license-keydocker compose --profile geoip up -d --buildSee docs/GEOIP.md.
Third-party map and data attribution is documented in THIRD_PARTY_NOTICES.md.
The Bandwidth tab separates payload traffic between users and BurrowGate from traffic between BurrowGate and origin servers. It provides time-series charts, HTTP/WebSocket totals, the busiest client IPs, one-click site blocking, and client bandwidth by country. Counters are streamed without buffering bodies and persisted as efficient one-minute aggregates.
See docs/BANDWIDTH.md.
Each site, and each route policy, can define a default IP action, a default country action, explicit IP or CIDR rules, explicit ASN rules, and explicit country rules. This supports blocklists, deny-by-default allowlists, and trusted clients that bypass browser verification - either site-wide or scoped to a single route, such as allowing one trusted IP on an API endpoint while blocking everyone else.
A route's network policy is checked first and takes precedence over the site's for requests matching that route; a route with nothing configured falls back to the site's policy:
- Longest matching IP or CIDR rule
- Explicit ASN rule
- Explicit country rule
- Default country action
- Default IP action
- Route policy access mode
There is no default ASN action - only explicit ASN allow/block/challenge/pass rules, falling through to country policy and the defaults when no ASN rule matches. Country and ASN policy each fail open independently when their respective GeoIP database is unavailable. IP rules and the default IP action continue to apply regardless.
Route policies can override a site's default behavior by path and HTTP method.
Available access modes:
inherit: use the site defaultchallenge: require the site or route challenge chainbypass: proxy without browser verificationblock: return HTTP 403 without contacting the origin
Example JSON API policy:
Name: JSON API
Path: /api/**
Access mode: Bypass browser verification
Rate limiter: Sliding window
Maximum: 120
Window: 60000 ms
Identity: IP address
This allows non-browser API clients to work normally while BurrowGate applies request limits at the edge.
Rate limits can use the client IP, a verified BurrowGate session, or a selected application header. Counters can be shared across the policy or separated by path and method.
The site's HTTP tab can set or remove request headers before an origin request and response headers before the origin response is returned to the client. Route policies can add their own rules. A route rule for the same header takes precedence over the site rule. Connection framing, public-host forwarding, client-IP forwarding, and signed X-BurrowGate-* identity headers remain proxy-managed and cannot be overridden.
Each site can also limit request-body bytes, request-target bytes (path plus query string), and combined parsed request-header bytes. A value of 0 is unlimited. Route limits are blank when inherited and can use 0 to explicitly remove the site limit. BurrowGate rejects violations with 413, 414, or 431, records a request-limited traffic event, and counts streamed request bodies so chunked uploads cannot bypass the configured maximum.
Static caching is disabled by default and can be enabled from the site's HTTP tab, with per-route enable, disable, TTL, object-size, and extension overrides. Entries live only in bounded process memory and are isolated by site, route-policy version, URL query, and accepted encoding. Origin max-age or s-maxage can shorten the configured TTL.
BurrowGate only considers GET assets with an allowed extension. Authorization, application cookies, range and conditional requests, explicit refreshes, non-200 responses, HTML or JSON, Set-Cookie, attachments, Content-Range, unsafe Vary, and private, no-store, or no-cache responses bypass storage. These checks use the original origin headers, so a downstream header policy cannot turn a private response into a cacheable response. HEAD can reuse an existing cached GET without storing a body.
Responses expose X-BurrowGate-Cache: HIT, MISS, or BYPASS. The dedicated Cache dashboard tab reports historical outcomes, hit ratio, origin requests avoided, top paths, runtime entries, memory, stores, evictions, expiry, and bytes served. Administrators can purge a site, a path prefix, one route policy, or every site. Site, route, and origin configuration changes purge affected entries automatically. Every applicable traffic event stores hit, miss, or bypass independently from its access decision, so a cache hit still remains classified as verified, authenticated, allowlisted, or unprotected traffic.
Body capture is disabled by default and can be enabled from the site's HTTP tab, with per-route enable, disable, size, content-type, and expiration overrides. Request and response bodies are limited independently in bytes, filtered to a configurable content-type allow-list (text-based types only, e.g. application/json), and can be given an expiration so capture stops automatically without leaving it on indefinitely. An instance-wide byte ceiling caps every configured limit.
Compressed bodies (gzip, deflate, brotli, zstd) are decompressed only for the captured copy. The response forwarded to the client is always the original, untouched bytes. A captured body is stored unredacted, so avoid enabling it on routes that handle credentials or other sensitive data unless that's the intent.
Click any row in Recent Traffic to open its full detail, including the captured request and response bodies when present, with a truncation indicator when a body exceeded its configured limit.
See docs/BODY_CAPTURE.md.
Header capture is disabled by default and can be enabled from the site's HTTP tab, with the same per-route enable, disable, and expiration overrides as body capture. Authorization, Cookie, and Set-Cookie are redacted by default (the header name is stored, the value isn't); this can be turned off, and an additional list of header names to redact can be configured, per site or per route.
CORS is disabled by default and can be enabled from the site's HTTP tab, with a per-route enable, disable, or inherit override. When enabled, BurrowGate answers cross-origin preflight (OPTIONS) requests directly - skipping the browser-verification challenge and access-list sign-in, since neither can complete during a preflight - and adds the matching Access-Control-* headers to real responses. A wildcard allowed origin cannot be combined with allowed credentials.
See docs/CORS.md.
HSTS is disabled by default and can be enabled from the site's HTTP tab. Unlike other HTTP policies it is site-only, since Strict-Transport-Security is a per-hostname browser directive rather than a per-path one. Once enabled, Strict-Transport-Security is added to every HTTPS response for that site, including cached, blocked, and error responses. Preload requires include-subdomains and a max age of at least one year.
See docs/HSTS.md.
Any row in Recent Traffic can be replayed from its detail view with manage access to the site. Captured headers and body are editable before sending, a redacted header can be filled in or overridden, and a same-site redirect is followed automatically (up to 10 hops) so testing a "created, fetch the result here" endpoint doesn't need a second manual resend. A redirect to a different host is left unfollowed, and the full hop-by-hop chain is shown in the result either way. Resending anything other than GET/HEAD prompts for confirmation first.
See docs/RESEND.md.
Each site can choose HTML or JSON for errors generated by BurrowGate. HTML mode provides an editable template with escaped placeholders and a reset-to-default action. JSON mode allows the administrator to select exactly which response fields are exposed.
Custom responses cover network blocks, route blocks, rate limits, verification-required API requests, origin failures, and WebSocket handshake failures. They do not replace successful origin responses; configured HTTP header and cache policies can still adjust their headers.
Each site can probe a path such as /health with a direct GET request to every enabled origin in its pool. A response from 200 through 299 is healthy; redirects, timeouts, connection errors, and other status codes are failures. Checks use a configurable interval (minimum 3 seconds), timeout, failure threshold, and recovery threshold. Individual origins can override the site health path.
Unhealthy origins are removed from normal selection while another usable origin exists. The default Keep proxying and alert behavior still attempts an origin if every health check is unhealthy. The optional maintenance behavior skips new HTTP and WebSocket origin connections when the complete pool is unhealthy and returns the site's custom error response with status 503, Retry-After, and error code origin_unhealthy. Unknown and degraded states never activate maintenance mode.
Every health-check result also feeds a per-minute latency graph (minimum, average, and maximum response time, plus a timed-out-checks percentage) shown on the site's Health tab, so a transient network issue between BurrowGate and an origin is visible in the data rather than only inferred from state transitions.
A dedicated Notifications dashboard (separate from the Site and Stream editors) configures a webhook per site and per Stream, with per-event-type subscriptions: individual origin up/down, whole-pool up/down, the BurrowGate host's own internet connectivity going down or recovering, and IP auto-bans. Deliveries go out to ntfy, Slack, Discord, or a signed generic JSON webhook, retry durably in order (a slow-to-recover event never gets overtaken by a later one to the same destination), and are recorded in a paginated, filterable log regardless of whether delivery succeeded.
A dedicated Firewall Sync dashboard pushes BurrowGate's own IP block rules out to an external firewall, so blocked traffic is dropped before it costs this host bandwidth or CPU. Every 10 seconds (configurable) it aggregates active block rules across every site and stream, dedupes them, and reconciles the result against each enabled provider, capped to that provider's own entry limit with oldest-bans-first eviction. UniFi Controller, local nftables, OVH's per-IP edge firewall, and AWS VPC Network ACLs are supported today.
Private/loopback ranges are never pushed, and an admin-managed whitelist adds another layer of protection against accidentally locking yourself out of the VPS - a provider cannot be enabled without at least one whitelist entry or an explicit acknowledgment of the risk.
Every site keeps its original URL as the primary origin and can add more origins from the site editor. Available algorithms are priority failover, round robin, and smooth weighted round robin. Priority failover chooses the lowest healthy priority number; weight controls proportional selection in weighted mode. An origin can be drained to keep existing sticky sessions while preventing new assignments.
With sticky affinity enabled, BurrowGate stores an origin ID on an existing visitor session. Requests without a valid session (including unprotected API calls) use a deterministic client-IP assignment without storing additional per-IP load-balancer state. If the assigned origin becomes unavailable, BurrowGate selects another origin and updates the session assignment. Safe GET and HEAD requests receive one connection-level failover retry; non-idempotent requests are never replayed automatically.
Each origin can also optionally present a client certificate (mTLS) when BurrowGate connects to it over HTTPS, and/or trust a specific certificate for validating that origin's server identity - the two are independent, so an origin whose software can't verify client certificates still benefits from origin-certificate trust alone. Both are disabled by default. BurrowGate can generate either certificate for you: the client certificate's private key never leaves BurrowGate, while a generated origin certificate's private key is handed to you once (to install on the origin) and is never stored. Either can also be uploaded from your own PKI instead.
See docs/ERROR_RESPONSES.md and docs/ORIGIN_MTLS.md.
Any origin (a site's primary origin or an additional one in its pool) can serve files straight from disk instead of proxying to a backend, so a plain static site or SPA doesn't need its own web server process. Folders are picked from a dashboard file browser confined to BurrowGate's static-sites directory (BG_STATIC_ROOT_DIR, created automatically on startup), never an arbitrary filesystem path.
Serving supports a configurable index file, optional SPA fallback (serves the index file for any unmatched path), clean URLs (/report resolves to report.html when there's no exact match), HTTP range requests for partial content, and conditional requests (ETag/Last-Modified, returning 304 Not Modified when unchanged). Static and proxy origins can be mixed in the same load-balanced pool; static origins are never health-checked (there's no backend to probe) and are always treated as available. Response headers still go through the site/route header policy and CORS/HSTS settings, and static responses are eligible for the same edge cache as proxied ones - BurrowGate doesn't set its own Cache-Control for static files, so caching stays fully under your control.
See docs/STATIC_SITES.md.
Each protected site can require a BurrowGate user login after the browser challenge. Users are global identities and can be linked to multiple sites without copying password hashes. Passwords are Argon2id-hashed, login attempts are rate limited, and password changes or disabling a user revoke their authenticated sessions.
Proxy authentication uses the existing HTTP-only BurrowGate session, so an application's Authorization: Basic or Authorization: Bearer header remains available to the origin. An optional setting sends the authenticated username in client-spoof-resistant, HMAC-signed identity headers and browser-readable signed cookies; passwords are never forwarded.
See docs/ACCESS_LISTS.md, docs/TWO_FACTOR_AUTH.md, and the separate frontend/API flow in docs/CROSS_SITE_AUTH.md.
A site or route stores an ordered challenge policy:
[
{
"provider": "pow-sha256",
"config": {
"difficulty": 18
}
}
]The challenge registry is designed to support additional providers such as CAPTCHA services without changing the proxy or session flow.
See docs/ADDING_CHALLENGES.md.
Each site supports:
- disabled TLS
- an uploaded certificate chain and private key
- automatic Let's Encrypt certificates
BurrowGate serves ACME HTTP-01 challenges directly from port 80 before redirects, route policies, IP rules, sessions, or browser challenges.
Private keys are encrypted with AES-256-GCM before they are stored in SQL. The encryption key is read from BG_MASTER_KEY, BG_MASTER_KEY_FILE, or the generated data/master.key file.
Back up the database and master key together. Losing the master key makes stored private keys unusable.
See docs/TLS.md.
Open the Streams dashboard from the switcher at the top of the control panel. Each stream has a name (used throughout the dashboard instead of a bare port number) and configures an incoming port, forward host and port, TCP and/or UDP, optional TCP TLS termination, an optional TCP origin health check, and its own monitoring-retention period.
The dashboard provides live TCP connections and UDP peers, connect/disconnect and error logs, client country and ASN, and payload totals grouped by IP, incoming port, and protocol. Because UDP has no transport connection lifecycle, BurrowGate opens a synthetic peer session on the first datagram and closes it after the configured inactivity timeout.
With the default network_mode: host Compose configuration, every stream port you create from the dashboard is immediately reachable on the host with no Compose changes or container restart. Firewall the host to only expose the ports you intend to publish.
Selecting a certificate terminates incoming TCP TLS and forwards decrypted bytes. Leaving the certificate empty performs raw TCP forwarding and therefore supports TLS passthrough. TLS/DTLS termination is not available for UDP.
Changing the incoming port, forward host/port, certificate, PROXY protocol mode, or a TCP/UDP toggle swaps the stream's listener; that can be scheduled for a chosen time instead of applying immediately, so it doesn't land in the middle of active connections. Every other stream setting still applies immediately. See docs/SCHEDULED_CHANGES.md.
See docs/STREAMS.md.
WebSocket upgrades pass through the same site, route, IP, rate-limit, and session checks as normal HTTP requests.
Protocol mapping is automatic:
http://origin.example.com -> ws://origin.example.com
https://origin.example.com -> wss://origin.example.com
BurrowGate forwards application cookies, authentication headers, binary messages, text messages, and negotiated subprotocols. BurrowGate credentials are removed before the upstream handshake.
After a successful challenge, BurrowGate creates a random opaque token and stores only its SHA-256 hash. Browsers receive an HTTP-only cookie.
API clients can use:
Authorization: Burrow <token>or:
X-Burrow-Token: <token>Sessions can be monitored and revoked from the dashboard.
Each website's monitoring-retention setting also governs expired/revoked sessions, challenge-flow history, expired network rules, certificate activity, origin health transitions, and notification events and deliveries. Challenge step secrets and expired admin sessions are removed as soon as incremental maintenance reaches them. Cleanup runs in small round-robin batches with pauses and a per-run time budget so retention work does not create a large database-latency spike.
BurrowGate signs origin headers with the site's signing secret:
X-BurrowGate-Verified: true
X-BurrowGate-Access-Mode: verified
X-BurrowGate-Session-Id: sess_...
X-BurrowGate-Client-Ip: 203.0.113.10
X-BurrowGate-Country: US
X-BurrowGate-Timestamp: 1785681000
X-BurrowGate-Signature: <HMAC-SHA256>X-BurrowGate-Country is XX for private/local addresses and ZZ when GeoIP is disabled or the country cannot be resolved.
Origins should reject direct public traffic. Use a private network, firewall allowlist, WireGuard, or mutual TLS so requests cannot bypass BurrowGate.
The dashboard includes:
- request volume, blocked requests, errors, and latency
- active, expired, and revoked sessions
- IP-rule activity and current rule state
- route-policy outcomes and configuration totals
- challenge-gated access lists with reusable users and signed upstream identity
- cross-site request and latency comparison
- an "All websites"/"All streams" option in the site and Stream selectors, combining statistics, graphs, and tables across every site or Stream a user can access
- interactive country map for requests and newly created sessions, plus a "Top ASNs" list by network provider
- server-side pagination, search, filters, and sorting
- exact From and To date-time selection shared by statistics, graphs, maps, traffic, and sessions, defaulting to the last 24 hours on load and on every site/Stream switch regardless of configured retention, so opening the dashboard never triggers a full-retention query
- drag-to-select time ranges directly on time-series graphs
- click-through detail on every Recent Traffic row, including captured request/response bodies and headers when enabled, with a Resend action to replay the request from the dashboard (see Resend)
- origin and Stream health-check latency graphs (minimum, average, maximum, and timed-out-check percentage)
- a dedicated Host dashboard page for everything scoped to the machine rather than a single site or stream: live-updating CPU, memory, disk, and network usage tiles (refresh interval configurable down to 1 second) alongside historical minimum/average/maximum graphs, and internet connectivity latency pinging public DNS resolvers directly from the BurrowGate host to help distinguish an origin problem from a network problem - both with their own threshold-based webhook notifications (see Notifications and
docs/SYSTEM_MONITORING.md), and working correctly in both bare-metal and Docker deployments
BurrowGate automatically selects a suitable graph bucket size for the chosen interval and limits the result to roughly 120 points. Missing intervals are returned as zero values so graphs remain stable during quiet periods. Dragging across a time-series graph applies the highlighted interval to the full dashboard.
Operational metrics can also be exposed in OpenMetrics format for Prometheus or an OpenTelemetry Collector. The exporter covers request volume and latency, payload bytes, Stream events and active connections, listener health, origin health checks and notification delivery, connectivity ping checks, Stream origin health checks, host/container CPU/memory/disk/network usage, monitoring queues, persistence failures, retention cleanup, database availability, GeoIP and ASN database status, and process memory. It deliberately excludes paths, client IPs, countries, ASNs, sessions, and usernames from labels. See docs/OPENMETRICS.md.
Managed request protection defaults to monitor mode and can be configured per site or overridden per route. Its dashboard separates clean, would-block, and blocked traffic and records versioned rule metadata without storing matching input values. See docs/MANAGED_PROTECTION.md.
Traffic retention is configured per site from 1 to 365 days. Maintenance removes expired events automatically.
Install dependencies:
bun installStart BurrowGate in watch mode:
bun run devStart the example origin server:
bun run originRun tests and TypeScript checks:
bun test
bun run typecheckRegenerate the compressed world map assets after changing public/world.svg:
bun run build:mapThe project uses .editorconfig and .prettierrc.json with tabs and a width of 2. YAML files use two spaces.
- ACME supports HTTP-01 only. Wildcard certificates require DNS-01 support.
- Route rate-limit counters are stored in memory and reset when the process restarts.
- Multiple gateway nodes do not share rate-limit counters yet.
- Detailed request events are stored in SQL. Very high traffic deployments should use an external log store.
BurrowGate is licensed under the GNU General Public License v3.0. See LICENSE.
