Cloudflare Zero Trust + Pi (recommended)
Production deployment: Cloudflare Zero Trust + Pi
Real-world IBKR API deploys hit a wall: api.ibkr.com (fronted by
Akamai) rejects connections from cloud datacenter IPs. So your
bezant-server can’t run on Railway / Fly / Render / Heroku and reach the
upstream successfully — IBKR responds 401 to the SSO→CPAPI bridge call,
and every typed API call cascades into 401s.
The pattern that works in 2026:
Your bot (Railway/cloud)
│ HTTPS + Service Token
▼
Cloudflare Zero Trust (Tunnel + Access)
│ Cloudflare Tunnel
▼
Raspberry Pi at home
│ ┌───────────────────────────────┐
│ │ bezant-server (port 8080) │
│ │ CPGateway (port 5000) │
│ └───────────────────────────────┘
│ Cloudflare WARP egress
▼
api.ibkr.com (Akamai)
Why each piece:
- Pi at home — residential ISP IP would also be flagged by Akamai if it weren’t for WARP. (Don’t skip WARP.)
- Cloudflare WARP on the Pi — routes outbound to api.ibkr.com through Cloudflare’s edge IPs, which are reputationally clean.
- Cloudflare Tunnel — exposes the Pi to your bot without opening a port on your router or having a public IP.
- Cloudflare Zero Trust Access — gates the public hostname. Browsers (you) hit an SSO challenge; service-to-service calls (your bot) carry a Service Token in two headers.
One-shot setup
# 1. On the Pi (Raspberry Pi 4/5 with 4GB+ RAM, Pi OS Lite arm64):
sudo apt update && curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
# 2. Install Cloudflare WARP (residential→clean-IP egress):
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] \
https://pkg.cloudflareclient.com/ bookworm main" | \
sudo tee /etc/apt/sources.list.d/cloudflare-client.list
sudo apt update && sudo apt install -y cloudflare-warp
warp-cli --accept-tos registration new
warp-cli --accept-tos connect
# 3. Install Cloudflare Tunnel (cloudflared) — get a token from the
# Zero Trust dashboard → Networks → Tunnels → Create:
sudo cloudflared service install <YOUR_TUNNEL_TOKEN>
# 4. Run bezant-combined (CPGateway + bezant-server in one container):
docker run -d --name bezant --restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-e BEZANT_DEBUG_TOKEN="$(openssl rand -hex 32)" \
ghcr.io/isaacrowntree/bezant-combined:latest
Cloudflare dashboard configuration
- Tunnel → add Public Hostname:
bezant.yourdomain.com→ serviceHTTP localhost:8080. - Access → Applications → add Self-hosted for the same
hostname. Add two policies:
- Browser (Allow): “Emails = [email protected]” — for you to do the IBKR login interactively.
- Service (Service Auth): generated Service Token — for your bot. Save the Client ID + Secret.
- Your bot calls
https://bezant.yourdomain.com/...with two headers:CF-Access-Client-Id: <client-id>.access CF-Access-Client-Secret: <secret>
Login flow
You’ll need to do an interactive IBKR login periodically — open
https://bezant.yourdomain.com/sso/Login in a browser, get challenged
by Cloudflare Access SSO, then by IBKR’s own login form, and approve
the 2FA push on your phone. Once that’s done, bezant-server’s
keepalive keeps the session warm and your bot’s API calls succeed.
How often you need to re-login depends on IBKR — community reports range from ~12h to a few days, and IBKR runs nightly maintenance that typically forces a fresh login once per trading day. Don’t assume a hard SLA; design your bot to handle a 401 by surfacing a “needs login” alert rather than crashing.
Security model
- Cloudflare Zero Trust is the primary perimeter. With a correctly configured Access policy, only your email-authenticated browser and your token-authenticated bot can reach the Pi.
- bezant-server’s
BEZANT_BINDdefaults to0.0.0.0:8080— that’s fine behind Cloudflare Tunnel + a127.0.0.1Docker port-bind (as in the snippet above). Don’t expose 8080 directly to the internet without Zero Trust in front. - Debug endpoints (
/debug/jar,/debug/probe) are off by default. SettingBEZANT_DEBUG_TOKENenables them, gated by anX-Bezant-Debug-Tokenheader (or?token=…query). With Zero Trust in front, this is defense-in-depth — leave it off until you’ve verified your Access policies are tight. - The shared cookie jar holds live IBKR session cookies. Anyone who can read it can resume the IBKR session and trade your account. bezant-server is single-tenant by design — don’t deploy this proxy multi-tenant.