2 Usage
maxpeterkaya edited this page 2026-09-23 12:40:40 -04:00
This file contains ambiguous Unicode characters

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_INTERVAL is set.
  • The js challenge alone is bypassable by a scripted client; use pow or credentials where that matters.
  • Check the license of any feed you point GUARD_IP_FEEDS or GUARD_UA_FEEDS at.

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.