documentation
behind cloudflare
Running GroundShade behind Cloudflare orange-cloud. Required trusted_proxies, what works, what gets silenced, and the Caddy recipe.
updated
Cloudflare in front of GroundShade is a supported topology since v0.7.0 (which fixed connection-cap saturation behind a fronting proxy). This page covers orange-cloud (CF terminating TLS). Grey-cloud is direct traffic and needs no special setup.
Orange-cloud works, but two security signals get silenced when CF terminates TLS at its edge. Confirm that trade-off matches your threat model before flipping it on.
Topology
internet → CF edge (TLS) → origin server
├─ fronting Caddy (HTTP)
│ ├─ GroundShade
│ │ └─ your app
│ └─ (or directly to your app)
From GroundShade’s perspective:
- Immediate TCP peer: your fronting Caddy.
X-Forwarded-Forchain:<real-client>, <cf-edge-ip>. CF appends its edge IP.- TLS handshake (JA4): CF’s edge fingerprint. The real client’s handshake terminates at CF.
Required config
listen.trusted_proxies must list both your fronting Caddy
and Cloudflare’s edge IP ranges. Without CF in the list, the
XFF parser walks right-to-left and stops at the CF edge IP,
treating it as the real client. Trust tokens, rate signals, and
trustless persistence would then key on CF’s edges instead of the
actual user.
listen:
http: "0.0.0.0:8080"
trusted_proxies:
# 1. Your fronting Caddy (pinned container IP).
- "172.18.0.10/32"
# 2. CloudFlare IPv4 edge ranges.
# Source: https://www.cloudflare.com/ips-v4
- "173.245.48.0/20"
- "103.21.244.0/22"
- "103.22.200.0/22"
- "103.31.4.0/22"
- "141.101.64.0/18"
- "108.162.192.0/18"
- "190.93.240.0/20"
- "188.114.96.0/20"
- "197.234.240.0/22"
- "198.41.128.0/17"
- "162.158.0.0/15"
- "104.16.0.0/13"
- "104.24.0.0/14"
- "172.64.0.0/13"
- "131.0.72.0/22"
# 3. CloudFlare IPv6 edge ranges.
# Source: https://www.cloudflare.com/ips-v6
- "2400:cb00::/32"
- "2606:4700::/32"
- "2803:f800::/32"
- "2405:b500::/32"
- "2405:8100::/32"
- "2a06:98c0::/29"
- "2c0f:f248::/32"
Verify the ranges before deploying. cloudflare.com/ips-v4 and cloudflare.com/ips-v6 are the source of truth; the ranges shift, so re-check before deploying.
Or set via env:
GROUNDSHADE_TRUSTED_PROXIES="$(curl -s https://www.cloudflare.com/ips-v4 | tr '\n' ',' | sed 's/,$//'),172.18.0.10/32"
Caddy config
Caddy needs its own trusted_proxies pointed at CF, otherwise it
strips CF’s CF-Connecting-IP and X-Forwarded-For instead of
forwarding them:
{
servers {
trusted_proxies static cloudflare
}
}
your-site.example.com {
reverse_proxy groundshade:8080 {
# Preserve XFF chain so GroundShade can walk through CF to the
# real client. Caddy appends its own IP to XFF here; the
# GroundShade-side trusted_proxies handles the rest.
header_up X-Forwarded-For {>X-Forwarded-For}, {remote_host}
}
}
trusted_proxies static cloudflare is a Caddy module that
auto-fetches and refreshes CF’s IPs. If you can’t use it, list the
CIDRs explicitly: trusted_proxies static <CIDR> <CIDR> ....
What works behind orange-cloud
| Feature | Status | Notes |
|---|---|---|
| Forwarding | ✓ | Just works |
| Connection-cap saturation | ✓ (v0.7+) | CF edges don’t count against the per-IP cap |
| Real client IP in logs | ✓ | XFF chain resolves to the original client |
| Trust tokens | ✓ | Bound to client /24 from XFF |
| Rate signal, per-IP arm | ✓ | Counts by real client /24 |
| Trustless persistence | ✓ | Per real client /24 |
| Defense ladder | ✓ | Detector sees real latency / 5xx |
| Manual shields-up | ✓ | Always works |
| Operator IP allowlists | ✓ | Resolved against real client IP |
| Operator UA allowlists | ✓ | UA is forwarded unchanged |
What doesn’t work (TLS termination limits)
| Feature | Status | Why | Workaround |
|---|---|---|---|
| JA4-keyed rate signal | ✗ silenced | CF terminates TLS; origin sees CF’s JA4 | Per-IP/24 arm still works |
| Verified-crawler fast lane (rDNS) | ✗ dead | rDNS runs on the peer IP (Caddy/CF), never resolves to googlebot.com | Use fastlane.allow_user_agents |
| ForgedBrowser classifier | ✗ degraded | Uses JA4 ↔ UA consistency check; with uniform JA4, every request looks consistent | Rely on rate signal + trustless persistence + UA patterns |
The dashboard will show JA4: detected because CF and Caddy are
forwarding some JA4. It’s CF’s, not the real client’s. The
per-JA4 counter in groundshade_signals_rate_tracked_keys{key="ja4"}
sits at a small number relative to per-IP keys; that’s the visible
fingerprint of the limit.
Recommended fastlane config behind CF
Because verified-crawler rDNS is dead behind CF, swap it for UA allowlists:
fastlane:
crawlers:
# rDNS verification can't see past CF; these flags do nothing here.
google: true
bing: true
duckduckgo: true
allow_user_agents:
# Substring match, case-insensitive. Spoofable; the rate-signal
# backstop catches mass abuse from anyone forging these.
- "Googlebot"
- "bingbot"
- "DuckDuckBot"
- "AppleBot"
- "UptimeRobot"
- "Uptime-Kuma"
If your threat model requires verified crawlers, don’t use orange-cloud. Grey-cloud preserves both JA4 and the rDNS fast lane.
Test the setup
-
Real client IP visible. Hit your site from a known external IP. Check the log:
docker logs groundshade | grep -E 'client_ip|method=' | head -3The
client_ip(orclient_ip_hash) should reflect your IP, not a104.16.x.x/172.64.x.x(CF edge) or172.18.x.x(Caddy bridge) address. -
Per-IP rejection counter stays flat. Under normal load:
curl -s -H "Authorization: Bearer $TOKEN" \ http://127.0.0.1:9090/metrics \ | grep 'connections_rejected_total{reason="per_ip"'Should sit at
0. If it climbs, your fronting Caddy’s IP is not intrusted_proxies. -
JA4 state acknowledges the limit.
curl -s -H "Authorization: Bearer $TOKEN" \ http://127.0.0.1:9090/admin/status \ | jq .ja4Says
"state": "detected"with few distinct JA4 keys tracked (about one per CF version). Expected. -
Trust tokens bind to real prefix. Solve a challenge from one IP, then send a second request from a different IP in the same
/24with the same cookie. It should pass (the token binds to/24). From a different/24: re-challenge expected.
Recommendation
Use grey-cloud (CF DNS-only) when you can. You get:
- Real client JA4. Behavioural signals key on the actual TLS fingerprint.
- rDNS verification. Known-good crawlers don’t see the wall.
- One less authority in your trust chain.
Use orange-cloud when you need CF’s edge protections (WAF, edge rate limiting, L3/L4 DDoS, image optimisation) and accept losing JA4-keyed detection. GroundShade still provides per-IP rate, trustless persistence, and the trust-token wall, on a reduced signal set.