This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This page is the full reference for web-proxy. It covers every environment variable, every CLI flag, and the behavior behind each run mode. See Home for an overview and Authentication for access control.
Configuration basics
web-proxy reads configuration from environment variables. Variables can be set in the shell, in a container manifest, or in a .env file placed in the working directory. Every variable has a default, so the proxy works without any configuration.
A minimal .env file looks like this.
PORT=8080
AUTH_TYPE=basic
AUTH_USER=admin
AUTH_PASS=<hex sha256 of your password>
Modes are chosen with CLI flags. Everything else is an environment variable.
Server
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
The port the proxy listens on for public traffic |
DOMAIN |
localhost |
The brand name shown on the session login page |
TRUST_XFF |
false |
Trust the X-Forwarded-For header for the client IP. Leave false unless web-proxy sits behind a trusted reverse proxy that overwrites the header |
Proxying
By default web-proxy forwards every request to the single upstream defined by PROXY_HOST and PROXY_PORT. Setting PROXY_TARGETS switches it to load balancing across several upstreams.
| Variable | Default | Description |
|---|---|---|
PROXY_HOST |
localhost |
The upstream host used when PROXY_TARGETS is unset |
PROXY_PORT |
3001 |
The upstream port used when PROXY_TARGETS is unset, and the port managed mode starts the app on |
PROXY_TARGETS |
unset | A comma separated list of host:port upstreams that enables load balancing |
PROXY_API_KEY |
unset | When set, attached as X-Api-Key to every outbound request web-proxy makes |
Each target in PROXY_TARGETS must include a port. A target without a scheme is treated as http://.
When PROXY_API_KEY is set, web-proxy adds X-Api-Key: <value> to every outbound request it makes: the proxied request to the upstream, OIDC discovery, token, and JWKS calls, and HTTP health-check probes. A request that already carries an X-Api-Key is left untouched, so a client's key is never overwritten. This lets web-proxy reach services that sit behind another web-proxy gate.
Two paths cannot carry the header: WebSocket and other upgrade requests, which bypass the outbound transport, and TCP health checks (PROXY_HEALTHCHECK_PATH unset). The browser redirect to an OIDC authorization endpoint also cannot send it, so a provider behind a gate still needs forms or basic for browsers. Only set PROXY_API_KEY when every outbound destination is trusted, since the key is sent to all of them.
Load balancing and health checks
With several targets configured, web-proxy round robins requests across the targets that pass a health check. By default the check is a TCP connection; set PROXY_HEALTHCHECK_PATH to probe an HTTP path instead. Targets start unhealthy and leave rotation after the first failed check. When every target is down the proxy answers with 503.
| Variable | Default | Description |
|---|---|---|
PROXY_HEALTHCHECK_ENABLED |
true |
Runs health checks and routes only to healthy targets |
PROXY_HEALTHCHECK_INTERVAL |
10 |
Seconds between health checks per target |
PROXY_HEALTHCHECK_TIMEOUT |
3 |
Seconds a connection attempt waits before the target counts as down |
PROXY_HEALTHCHECK_PATH |
(empty) | HTTP path to GET (e.g. /health); empty keeps the TCP-only check. Any status below 400 counts as healthy |
Set PROXY_HEALTHCHECK_ENABLED to false to distribute across all targets unconditionally. The load balancer example demonstrates failover with two upstreams.
Managed application mode
The -app flag makes web-proxy start your application itself. It reads the start:web script from package.json, or falls back to the start script, and runs that command. The app is then reachable only through the proxy.
| Flag | Default | Description |
|---|---|---|
-app |
false |
Starts the web application from package.json |
-cmd |
unset | Overrides the start command instead of reading package.json |
-log |
true |
Streams the application stdout into the proxy output |
The proxy listens on PORT and forwards to the app on PROXY_PORT. A NextJS start command that has no explicit port flag gets -p appended automatically, so next start runs on PROXY_PORT. Other frameworks must be told to listen on PROXY_PORT themselves. When -log is false the application stdout is suppressed, while stderr always passes through.
Static files
The -static flag serves a frontend build instead of proxying.
| Flag | Default | Description |
|---|---|---|
-static |
false |
Serves static files at the root path |
-static-dir |
dist |
The folder that is served in static mode |
The static website example shows a Dockerfile that builds a frontend and serves it through static mode.
Blocking
Blocked requests never reach the upstream and receive a 402 response.
| Variable | Default | Description |
|---|---|---|
BLOCK_USER_AGENTS |
unset | Comma separated user agent substrings that are blocked, matched case-insensitively |
BLOCK_IPS |
unset | Comma separated client IPs or CIDR ranges that are blocked |
A user agent is blocked when its value contains any configured entry as a substring, ignoring case, so BLOCK_USER_AGENTS=bot,curl blocks Googlebot/2.1 and curl/8.0 as well as a full user agent string. An entry matches itself, so exact values still work.
BLOCK_IPS accepts a bare address (1.2.3.4, ::1) or a CIDR range (10.0.0.0/8, 2001:db8::/32). An entry that is neither an IP nor a valid CIDR is ignored with a warning.
Client IPs come from the direct network peer, matching the IP used in request logs. Set TRUST_XFF to true to take the client IP from the X-Forwarded-For header instead, which is only safe when a trusted reverse proxy in front rewrites that header.
The blocking example shows both lists wired into a container.
Bot guard
BLOCK_* is a fixed, hand-curated deny list. The bot guard is the layer above it: it scores every request from static and behavioural signals, and a severity tier decides whether to pass, throttle, block, tarpit, or challenge. It runs before authentication, so bots are turned away before they reach the login page. A request that carries valid credentials (bearer/API key, basic, forms session, or OIDC) is never challenged or tarpitted, though it is still scored and logged.
The guard is off by default. Turn it on with GUARD_LEVEL.
| Tier | What it does |
|---|---|
off |
Disabled (default) |
permissive |
Hard-block a high score (403) and throttle scan/rate abuse (429) |
balanced |
Blocks from a lower score (403), same throttling |
strict |
Challenge mid scores (JS), tarpit high scores, block the worst (403) |
paranoid |
Challenge every non-verified visitor (proof-of-work), tarpit and block aggressively; fails closed |
Scoring
Each request accumulates a risk score. Scores are additive; the tier sets the cutoffs that map a score to an action. Verified good crawlers and credentialled clients are handled specially.
| Signal | Score |
|---|---|
| Curated bad IP feed hit | +100 |
| Curated bad user-agent feed hit | +80 |
Honeypot path request (configured or from the upstream robots.txt) |
+80 |
| Baseline scraper/tool user agent | +40 |
Sensitive-path request (/.env, /wp-login.php, /actuator/*, …) |
+30 |
| 404 burst from one IP | +30 |
| Per-IP request rate over the threshold | +25 |
| Empty user agent | +20 |
Browser user agent missing Accept-Language / Accept-Encoding / Sec-Fetch-* |
+15 each |
| Verified good crawler (rDNS) | −60 |
Configuration
| Variable | Default | Description |
|---|---|---|
GUARD_LEVEL |
off |
off, permissive, balanced, strict, or paranoid |
GUARD_MODE |
enforce |
observe logs the action the guard would take without enforcing it. Use it to tune thresholds on live traffic |
GUARD_CHALLENGE |
tier preset | js, pow, or none to disable challenging. Defaults to js for strict and pow for paranoid |
GUARD_CHALLENGE_TTL |
30m |
How long a passed challenge cookie lasts |
GUARD_POW_DIFFICULTY |
4 |
Leading hex zeros a proof-of-work must produce |
GUARD_RATE |
100 |
Requests per GUARD_RATE_WINDOW before the rate signal fires |
GUARD_RATE_WINDOW |
1m |
Window for the rate, 404 and scan counters |
GUARD_SCAN_THRESHOLD |
3 |
Sensitive-path hits per window before a 429 |
GUARD_404_THRESHOLD |
20 |
404s per window before the 404-burst signal |
GUARD_TARPIT_MS |
5000 |
How long a tarpitted request is held |
GUARD_TARPIT_MAX |
256 |
Concurrent tarpitted requests; beyond this the client gets an immediate 403 |
GUARD_ROBOTS_TXT |
true |
Derive honeypot paths from the upstream robots.txt Disallow rules |
GUARD_HONEYPOT_PATHS |
unset | Extra honeypot paths (exact, glob, or /* prefix) |
GUARD_GOOD_BOTS |
major crawlers | rDNS suffixes trusted as crawlers, e.g. googlebot.com,search.msn.com |
GUARD_BLOCK_SCORE / GUARD_CHALLENGE_SCORE / GUARD_TARPIT_SCORE |
tier preset | Override the score cutoffs for every tier |
GUARD_IP_FEEDS / GUARD_UA_FEEDS |
unset | Feed URLs used only by the build-time generator and the optional runtime refresh |
GUARD_FEED_REFRESH_INTERVAL |
0 |
0 keeps runtime network access off; set e.g. 6h to refresh feeds in memory |
Curated feeds
The curated IP and user-agent lists are embedded in the binary, so the running proxy makes no feed network calls by default. Populate them at build time with go generate ./..., which reads GUARD_IP_FEEDS and GUARD_UA_FEEDS and writes guarddata/. A bare IP or CIDR goes on each IP line; a user-agent line is a substring, or a /regex/. Release builds regenerate the data so each release carries fresh feeds. Set GUARD_FEED_REFRESH_INTERVAL to let a running binary pull updates into memory between releases; if a refresh fails the last good feeds stay active.
IP feeds are matched through a sorted range table with a binary search, so lists with hundreds of thousands of CIDRs stay fast.
Challenges
js serves a page that waits briefly and then requests a signed pass cookie; pow additionally requires the browser to find a hash with a number of leading zeros (GUARD_POW_DIFFICULTY), which costs a scripted bot real CPU. Both set an HttpOnly, signed cookie for GUARD_CHALLENGE_TTL. The challenge endpoint is /__guard/challenge and is exempt from both the guard and authentication.
Good bots, tarpit and failure
Claimed crawlers (Googlebot, Bingbot, DuckDuckGo, Applebot, Yandex) are verified by reverse DNS plus forward confirmation and cached for an hour; a verified crawler is discounted and never blocked or challenged. The tarpit holds a request for GUARD_TARPIT_MS to cost a bot time; it never holds more than GUARD_TARPIT_MAX at once. When the guard itself errors, tiers below paranoid fail open and paranoid fails closed.
Guard activity is attached to the request log as guard_level, guard_score, guard_signals, and guard_action.
Ceilings
- State (rate, 404, scan and challenge-failure counters) is in-memory, so it is per-instance. Running several replicas behind a load balancer divides the counters between them.
- Embedded feeds age until the next release unless
GUARD_FEED_REFRESH_INTERVALis set. - The
jschallenge alone is bypassable by a scripted client; usepowor credentials where that matters. - Check the license of any feed you point
GUARD_IP_FEEDSorGUARD_UA_FEEDSat.
Request logs
Log entries are emitted as JSON on stderr. A successful request logs at info level and a failed one at error level with the same fields. Each entry includes the fields shown in Home, such as the request ID, latency, bytes, the upstream target, and the route or asset path. The application stdout in managed mode is plain text that passes through separately, so structured proxy logs stay valid JSON for ingestion by Grafana Alloy, Promtail, or any JSON collector.