Friendly Guard Proxy is a Go reverse proxy that protects your application from bot traffic and abuse. It is deployed in front of one upstream application and requires no changes to that application.
The proxy handles route enforcement, calls the Friendly Guard decision API, serves the returned
interstitial (explained below), and issues a local pass cookie after an ALLOW decision. The API owns decision IDs,
browser-check and challenge state, proof validation, and interstitial HTML. The state exchanged with the browser is an
opaque screening context; Friendly Guard Proxy does not parse or persist it.
Browser -> Load balancer (optional) -> Friendly Guard Proxy -> Upstream application
|
+-------------> Friendly Guard API
- Go 1.26 or newer when building from source.
- A Friendly Guard sitekey and server-side API key.
- One HTTP or HTTPS upstream origin.
- TLS termination in front of Friendly Guard Proxy for production deployments. The binary itself serves HTTP.
Build the binary from this directory:
go build -o friendly-guard-proxy ./cmd/friendly-guard-proxyCreate a friendly-guard-proxy.yml configuration file. You can base it on the example in example.friendly-guard-proxy.yml.
Start Friendly Guard Proxy:
FRIENDLY_GUARD_API_KEY=<API_KEY> ./friendly-guard-proxyUse -config /path/to/config.yml to load a different file and -log-level debug to enable per-request debug logs.
Environment variables are expanded before YAML is decoded. Unknown YAML fields are rejected, and the proxy refuses to start without at least one guarded route. This prevents a misspelled security setting from silently disabling protection.
| Field | Required | Default | Description |
|---|---|---|---|
server.listen |
No | :8080 |
Address on which the proxy serves HTTP. |
upstream.origin |
Yes | HTTP or HTTPS upstream URL. A path in this URL becomes a base path for proxied requests. | |
friendly_guard_api.api_endpoint |
No | eu |
eu (https://eu.frcapi.com), global (https://global.frcapi.com), or a full Friendly Guard API URL. |
friendly_guard_api.sitekey |
Yes | Sitekey protected by this proxy instance. | |
friendly_guard_api.api_key |
Yes | API key used to authenticate requests to the Friendly Guard decision API. | |
friendly_guard_api.timeout_seconds |
No | 5 |
Timeout for each decision API request. |
pass_signing_secret |
No | Generated | Secret used to sign local pass cookies. Must be at least 32 bytes when set. If unset, the proxy generates an ephemeral 32-byte secret. |
pass_request_limit |
No | 100 |
Protected upstream requests allowed per pass before re-screening. Set to 0 to disable rate limiting. |
guarded_routes |
Yes | Non-empty list of Go regular expressions identifying protected URL paths. | |
trusted_proxies |
No | Empty | CIDR ranges allowed to supply trusted forwarding headers. |
failure_mode |
No | open |
Behavior for transient decision failures and unknown outcomes: open or closed. |
dry_run |
No | false |
Record decisions without enforcing valid BLOCK or CHALLENGE outcomes. |
block_redirect_url |
No | Full HTTP(S) URL to redirect the browser to after a BLOCK decision. |
Changing pass_signing_secret invalidates all existing pass cookies. A configured secret must be at least
32 bytes. Every replica must use the same secret. A pass signed by one replica is rejected by another replica
that has a different secret, and the browser is sent through screening again. If the secret is unset, the proxy
logs a warning and generates an in-memory 32-byte secret for the lifetime of that process, so pass cookies are
invalidated on restart and are not valid on any other replica. Set an explicit secret before running more than
one replica.
Friendly Guard Proxy makes its enforcement decision before proxying a request.
- The proxy handles the exact paths
/__friendly-guard/v1/healthzand/__friendly-guard/v1/continueitself. These exact paths are never sent upstream. - Checks the
friendly_guard_passcookie. Invalid, expired, wrongly bound, or wrong-site cookies are cleared. - Checks the request path against every
guarded_routesexpression. - An unprotected route or an
OPTIONSrequest is proxied immediately. - A protected request with a valid pass is proxied if its request limit has not been exhausted (or if rate limiting is disabled).
- An exhausted pass is cleared and treated like a missing pass.
- A protected request without a pass is handled according to its method:
GETenters the decision flow.HEAD,POST,PUT,PATCH,DELETE, and other methods receive403 Forbidden.
Friendly Guard Proxy does not buffer and replay unsafe request bodies through an interstitial. Applications should first serve a
protected GET that obtains a pass, after which forms and API calls on protected routes can proceed normally.
Queries are not part of route matching. Friendly Guard Proxy tests each regular expression against both the original URL path and its
path.Clean normalized form. The request is protected if either form matches.
For example, with ^/protected(/.*)?$, all of these requests are protected:
/protected/
/protected/../public
/public/../protected
/public%3F/../protected
This prevents the proxy and an upstream router from reaching different security decisions when one of them
normalizes dot segments. The proxy still forwards the original path unchanged; normalization is only an
additional protection check.
Encoded question marks (%3F) are path characters and remain part of path normalization; actual query strings are excluded.
Anchor route expressions with ^ and $ when the whole path should match.
For a protected GET without a valid pass, Friendly Guard Proxy sends POST /api/v2/guard/prescreen to the configured API endpoint.
The request contains the sitekey and a request context with:
- Canonical client IP.
- Method, scheme, host, and normalized path.
User-Agent,Accept,Accept-Language,Signature,Signature-Input,Signature-AgentandSec-Fetch-*request headers.
Authorization headers, cookies, request bodies, and query strings are not included in this context.
The prescreen decision has one of these outcomes:
| Outcome | Friendly Guard Proxy behavior |
|---|---|
ALLOW |
Set a 15-minute pass cookie and proxy the original request. |
BLOCK |
Redirect to block_redirect_url when configured; otherwise return a non-cacheable 403 Forbidden page. |
CHECK |
Return the non-cacheable interstitial HTML on the protected URL. |
An interstitial is a temporary page shown in place of a requested protected page while Friendly Guard checks the browser.
The interstitial runs the Friendly Captcha SDK on the protected URL. It obtains a risk token and posts it, together with the opaque screening context, to the same-origin endpoint:
POST /__friendly-guard/v1/continue
The continuation endpoint forwards the sitekey, screening context, and proof fields to POST /api/v2/guard/decide.
| Outcome | Friendly Guard Proxy behavior |
|---|---|
ALLOW |
Set the pass cookie and return { "outcome": "ALLOW" }. The interstitial reloads the current protected URL. |
BLOCK |
Do not issue a pass. The interstitial redirects to block_redirect_url when configured; otherwise it displays the blocked state. |
CHALLENGE |
Return the rotated screening context. The interstitial starts the Friendly Captcha widget and submits its response through the same continuation endpoint. |
After a successful challenge, the final ALLOW response sets the pass cookie and the browser reloads the same URL.
The proxy does not store the original URL and does not use a redirect query parameter.
Set dry_run: true to evaluate Friendly Guard without enforcing BLOCK or CHALLENGE decisions and without presenting captcha
challenges. Protected non-GET methods without a pass still receive 403 Forbidden and never reach the decision API. The proxy still
performs prescreening for protected GET requests and returns the browser-check interstitial for CHECK, allowing the decision API
to evaluate the risk token. A valid BLOCK or CHALLENGE response is then treated as ALLOW: the proxy issues a pass and the browser
continues to the protected route. The decision API thus still receives the requests and records its original outcomes before the proxy overrides enforcement.
Because dry run allows CHALLENGE instead of presenting the captcha, it cannot observe challenge-completion results. In particular,
challenge-phase blocks caused by invalid, expired, duplicate, or unsuccessful captcha responses do not occur in this mode. Use the
number of CHALLENGE decisions as the measure of requests that would have experienced additional friction.
Friendly Guard Proxy issues a stateless JWT in the friendly_guard_pass cookie. The cookie has fixed behavior:
HttpOnlySameSite=LaxPath=/Securewhen the proxy determines that the external request used HTTPS
The JWT is signed with HS256 using the configured pass signing secret. It contains issued-at and expiry timestamps, is scoped to the configured sitekey through its audience, and is bound to the browser's User-Agent.
Every pass, including fail-open passes, has a fixed lifetime of 15 minutes. The cookie is only a clearance signal; it must not be treated as application authentication or authorization by the upstream.
When rate limiting is enabled, the proxy keeps a bounded in-memory request count keyed by each signed
pass's JWT ID (jti). The protected request that receives a pass from prescreen counts as the first request; a pass
received from the continuation endpoint starts counting when the interstitial reloads the protected page. After
pass_request_limit protected requests, the next protected GET starts a new screening flow. Unsafe methods continue
to receive 403 until a new pass is obtained.
Request counts are local to each proxy process and are not retained across restarts. Each replica enforces
pass_request_limit on its own, so the effective limit across a fleet is higher than the configured value.
When TLS terminates at a load balancer, that load balancer must be configured as a trusted proxy and must set X-Forwarded-Proto: https for the proxy to add the Secure attribute.
failure_mode applies to network errors, timeouts, decision API 5xx responses whose body is valid JSON, and unknown decision outcomes.
Friendly Guard Proxy issues a 15-minute pass when a transient failure occurs:
- During prescreen, the proxy sets the pass and proxies the original request.
- During continuation, the proxy sets the pass and returns
ALLOWso the interstitial can reload without looping.
Friendly Guard Proxy does not issue a pass and returns an error response.
Decision API 4xx responses, malformed JSON, missing required response fields, and invalid client continuation payloads
never fail open.
Fail-open improves availability but deliberately permits traffic during a Friendly Guard outage. Choose the mode as part of the application's security and availability policy.
Friendly Guard Proxy uses Go's httputil.ReverseProxy and preserves ordinary reverse-proxy semantics:
- Request methods, bodies, query strings, and the original path are forwarded.
- Upstream response status, headers, cookies, body, and streaming behavior are preserved.
- Hop-by-hop headers are removed by the Go reverse proxy.
- The
Forwardedheader is removed and is not rewritten. - An unavailable upstream returns
502 Bad Gateway.
upstream.origin determines the outbound scheme and host. Its optional path is prepended as a base path. The outbound
Host is the upstream host, while the original external host is sent as X-Forwarded-Host.
The proxy replaces the canonical proxy identity headers before forwarding:
X-Real-IP: the resolved client IP.X-Forwarded-For: the sanitized client and trusted-proxy chain, with the direct peer appended.X-Forwarded-Host: the external host.X-Forwarded-Proto: the external scheme.
Upstream applications should use these canonical headers from the proxy and must not prioritize unrelated client-IP headers that may have arrived from the public request.
Forwarding headers are attacker-controlled unless the direct socket peer is trusted. Friendly Guard Proxy therefore uses
trusted_proxies as a network trust boundary.
When the direct peer is not trusted:
X-Forwarded-For,X-Forwarded-Host, andX-Forwarded-Protoare ignored.- The socket peer is the client IP.
- The inbound host and direct TLS state determine the external host and scheme.
When the direct peer is trusted:
- Friendly Guard Proxy scans
X-Forwarded-Forfrom right to left, skips configured trusted proxies, and selects the first untrusted IP as the client. If every address in the chain is a trusted proxy, the leftmost address is the client. - If
X-Forwarded-Foris absent, the proxy accepts a valid singleX-Real-IPvalue from the trusted peer as the client. - Values to the left of that client are discarded because they may have been supplied by the visitor. Malformed values in that discarded portion are ignored.
- A malformed address encountered while scanning from the right, including a malformed client address, invalidates XFF and causes the proxy to fall back to the socket peer.
- The proxy accepts bare IPv4 and IPv6 addresses in XFF. Configure the load balancer not to include client ports.
- The proxy uses the first
X-Forwarded-Hostvalue and the firstX-Forwarded-Protovalue. It does not reject additional header values, a comma-separated list, or a scheme other thanhttporhttps. The trusted proxy should still send one value for each, andX-Forwarded-Proto: httpswhen the external request used HTTPS.
Configure the narrowest possible CIDRs for the load balancer or proxy instances. If Friendly Guard Proxy directly
receives public traffic, leave trusted_proxies empty so all forwarding headers are ignored.
Returns 200 OK with the body ok followed by a newline. This is a process health check and does not call the upstream or Friendly Guard API.
Reserved for the browser-check and challenge flow. It only accepts application/json and is never proxied upstream.
Friendly Guard Proxy handles SIGINT and SIGTERM with graceful HTTP shutdown. Use -log-level with debug, info,
warn, or error to control log verbosity. Debug logs include request-level routing and decision logs; normal
informational logs are limited to startup and significant events. API keys, pass cookies, screening contexts, risk
tokens, and captcha responses are not logged.
go build -o friendly-guard-proxy ./cmd/friendly-guard-proxyUse ./friendly-guard-proxy -version to print the version and build metadata.
Source builds default to version 0.0.0; GoReleaser injects the release version,
commit date, and full commit hash.
To build release artifacts locally without publishing, install GoReleaser v2 and run:
goreleaser release --snapshot --cleanArtifacts are written to dist/.
go test ./...The tests start local HTTP servers and do not need a running upstream, a Friendly Guard API, or a configuration file.
Friendly Guard Proxy is licensed under the MIT License.