Generate interactive network topology visualizations for your Cloudflare Zero Trust infrastructure. See your tunnels, access applications, policies, identity providers, and private networks as a beautiful, interactive graph.
Inspired by tailscale-network-topology-mapper
→ topology.dautovri.com — live demo with synthetic data, no login, no API token.
Also reachable at cloudflare-topology.pages.dev.
Want it on your own Cloudflare account with real data? See Deploy your own below (~2 minutes).
| Resource | Visualization |
|---|---|
| 🔵 Tunnels | Cloudflare Tunnels with connector status, ingress rules, origin IPs |
| 🟢 Access Applications | Self-hosted apps, SaaS apps with domains and session duration |
| 🟡 Access Policies | Allow/deny/bypass rules with include/exclude/require logic |
| 🟠 Access Groups | Reusable identity groups with member criteria |
| 🟣 Virtual Networks | Private network segmentation for tunnel routing |
| 🔴 WARP Devices | Enrolled devices with user info and last seen status |
| 🌐 Identity Providers | Okta, Azure AD, Google, GitHub, and other IdPs |
| 🛡️ Gateway Rules | DNS and HTTP firewall policies (optional) |
- Search: Find nodes by name, domain, or type
- Filter: Toggle visibility by resource type
- Zoom & Pan: Navigate large topologies
- Drag: Rearrange nodes manually
- Tooltips: Hover for detailed configuration info
- Dark Theme: Easy on the eyes, matches Cloudflare dashboard
- Python 3.12+
- Cloudflare account (any auth method below)
git clone https://github.com/dautovri/cloudflare-topology.git
cd cloudflare-topology
pip install -r requirements.txtPick whichever is easiest — the tool tries them in order:
Option A — Zero config (recommended): If you have wrangler installed:
wrangler login # one-time: opens browser, click Allow
python main.py # done — uses wrangler's OAuth token automaticallyOption B — Guided setup: Interactive wizard writes a .env file for you:
python main.py --setup # opens the token page, prompts for token, saves .env
python main.pyOption C — .env file: Copy the template and fill in your token:
cp .env.example .env
# edit .env, set CLOUDFLARE_API_TOKEN=...
python main.pyOption D — Environment variable:
export CLOUDFLARE_API_TOKEN="your-api-token"
python main.pyOption E — Try it without an account:
python main.py --demo # synthetic topology, no Cloudflare API callsAccount ID is auto-discovered from your token. Set
CLOUDFLARE_ACCOUNT_IDonly if your token has access to multiple accounts and you want a specific one.
📍 Where to create an API token
- Go to Cloudflare Dashboard → API Tokens
- Click "Create Token" → "Create Custom Token"
- Add permissions: Zero Trust: Read, Access: Apps and Policies: Read
# Basic usage - opens in browser
python main.py
# Skip tunnel configs for faster generation
python main.py --no-tunnel-configs
# Include Gateway firewall rules
python main.py --include-gateway
# Debug mode with verbose output
python main.py --debugShip a static version of the topology to your own Cloudflare Pages project — free tier, custom domain, HTTPS included.
npm install -g wrangler # Cloudflare's CLI
wrangler login # browser OAuth# First time: pick a unique project name on your account
PAGES_PROJECT=my-cf-topology make deploy-demo
# Subsequent deploys: just
make deploy-demoWrangler prints a *.pages.dev URL. Re-run any time you want to refresh.
export CLOUDFLARE_API_TOKEN=your-token
PAGES_PROJECT=my-cf-topology make deploy
⚠️ Static output contains your tunnel names, app domains, and policy details. Do NOT deploy real topology data to a public site. Use a Cloudflare Access policy (see below) to keep it behind auth.
Once a Pages project is deployed, point a domain at it:
# If your DNS is on Cloudflare (same account):
npx wrangler pages deployment tail # confirm project
# Then in the dashboard: Pages → your project → Custom domains → Set up a custom domain
# Enter e.g. topology.yourdomain.com — Cloudflare creates the CNAME automatically.
# If DNS is elsewhere: add a CNAME record
# topology.yourdomain.com CNAME YOUR-PROJECT.pages.devCloudflare issues the TLS cert automatically (typically under a minute).
For a real-topology deploy you want behind auth:
- Cloudflare Zero Trust dashboard → Access → Applications → Add an application → Self-hosted
- Application domain:
topology.yourdomain.com - Add a policy:
Emails: [you@yourdomain.com]→ Action: Allow - Save.
Now only authenticated users can view the deployed topology.
Run as a web service with automatic regeneration:
# Build
make build
# Run (requires API token — wrangler login not available in Docker)
export CLOUDFLARE_API_TOKEN="your-token"
make run
# View at http://localhost:8080Or with docker directly:
docker build -t cloudflare-topology .
docker run -d \
--name cloudflare-topology \
-p 8080:8080 \
-e CLOUDFLARE_API_TOKEN="your-token" \
cloudflare-topology| Endpoint | Method | Description |
|---|---|---|
/ |
GET | View topology visualization |
/health |
GET | Health + freshness status (JSON) |
/regenerate |
POST | Queue a topology refresh (requires Bearer auth) |
| Variable | Default | Description |
|---|---|---|
CLOUDFLARE_API_TOKEN |
(required) | API token with Zero Trust read permissions |
CLOUDFLARE_ACCOUNT_ID |
auto-discover | Account ID (required only for multi-account tokens) |
PORT |
8080 |
Port the Flask server binds to |
HOST |
0.0.0.0 |
Interface the Flask server binds to |
REGEN_INTERVAL_SECONDS |
900 |
Scheduled regeneration interval in seconds. 0 disables the scheduler. Valid range: 0 or >= 60. Recommended >= 300. |
REGEN_AUTH_TOKEN |
(unset) | Bearer token required for POST /regenerate. When unset, /regenerate returns 403. |
A background scheduler calls python main.py every REGEN_INTERVAL_SECONDS seconds (default: 15 minutes) and atomically replaces network_topology.html. Default behaviour: the container stays fresh with zero operator action.
- Disable: set
REGEN_INTERVAL_SECONDS=0. - Customise:
docker run -e REGEN_INTERVAL_SECONDS=1800 ...for 30 minutes. - Startup: if
network_topology.htmldoes not exist yet, the server generates it synchronously on boot before accepting requests.
The scheduler uses in-process state (threading.Timer + a lock). Do not run with multiple workers (gunicorn -w 2+, uwsgi with processes, etc.) — each worker would run its own scheduler and race for the output file.
The server detects common multi-worker env vars (WEB_CONCURRENCY, GUNICORN_WORKERS, UWSGI_WORKERS, GUNICORN_CMD_ARGS with -w N) and refuses to start the scheduler, logging an ERROR. The HTTP endpoints still work; the topology just won't auto-refresh.
For multi-replica deployments, coordinate regeneration externally (cron, K8s CronJob, CI).
POST /regenerate
Authorization: Bearer <REGEN_AUTH_TOKEN>
| Status | Meaning | Body |
|---|---|---|
202 Accepted |
Regeneration queued on background thread | {"status":"accepted","message":"Topology regeneration queued"} |
409 Conflict |
Another regeneration is already running. Sets Retry-After: 10. |
{"status":"already_running","hint":"GET /health returns regen_in_progress and next_scheduled_regen_at"} |
401 / 403 |
Missing or invalid auth | {"status":"error","message":"..."} |
Fire-and-forget: the response returns as soon as the job is queued, not when the topology is ready. Poll /health to detect completion.
{
"status": "healthy",
"topology_exists": true,
"last_generated_at": "2026-04-16T14:15:00Z",
"regen_in_progress": false,
"next_scheduled_regen_at": "2026-04-16T14:30:00Z"
}| Field | Type | Description |
|---|---|---|
status |
string | Always "healthy" when the server is up |
topology_exists |
bool | true if network_topology.html is present on disk |
last_generated_at |
ISO 8601 UTC or null |
Timestamp of the last successful regeneration (falls back to file mtime at startup) |
regen_in_progress |
bool | true while a regeneration is running |
next_scheduled_regen_at |
ISO 8601 UTC or null |
When the scheduler will fire next. null if the scheduler is disabled. |
Check freshness from outside the container:
curl -s http://localhost:8080/health | jq '.last_generated_at, .next_scheduled_regen_at'If last_generated_at is older than 2 * REGEN_INTERVAL_SECONDS, something is wrong — check container logs:
docker logs <container> 2>&1 | grep -iE 'scheduler|regenerat'Scheduler thread is named topology-scheduler for legible stack dumps.
POST /regeneratenow returns202 Accepted(queued, fire-and-forget) instead of200 OKafter synchronous completion. Clients that only check2xxkeep working. Clients that hard-checkstatus == 200must accept202.- Response body key changed from
{"status":"success", ...}to{"status":"accepted", ...}on success. - New
409 Conflictresponse when a regeneration is already running; clients should respectRetry-After: 10. /healthgains three new fields (last_generated_at,regen_in_progress,next_scheduled_regen_at). Existingstatusandtopology_existsfields are unchanged.- Output file writes are now atomic (
tempfile+os.replace). No action required; readers will never observe a truncated file.
Create a token at Cloudflare Dashboard → API Tokens:
| Permission | Scope | Used For |
|---|---|---|
| Zero Trust: Read | Account | Tunnels, virtual networks, routes |
| Access: Apps and Policies: Read | Account | Applications, policies, groups |
| Permission | Scope | Used For |
|---|---|---|
| Devices: Read | Account | WARP device enrollment |
| Gateway: Read | Account | DNS/HTTP firewall rules |
🛡️ Recommended: Use a Custom Token
Create a token with only the permissions you need. Avoid using Global API Keys.
- Go to API Tokens
- Click "Create Token"
- Use "Create Custom Token"
- Add the permissions listed above
- Set appropriate TTL and IP restrictions for security
| Type | Color | Shape | Description |
|---|---|---|---|
| Cloudflare | Orange | Star | Central hub node |
| Tunnel | Blue #3b82f6 |
Hexagon | Cloudflare Tunnel connectors |
| Application | Green #22c55e |
Circle | Access-protected applications |
| Policy | Yellow #eab308 |
Triangle | Access policy rules |
| Group | Orange #f97316 |
Circle | Access groups |
| Identity Provider | Cyan #06b6d4 |
Star | Okta, Azure AD, etc. |
| Virtual Network | Purple #a855f7 |
Square | Private network segments |
| Route | Lime #84cc16 |
Circle | Private network routes |
| Device | Red #ef4444 |
Diamond | WARP-enrolled devices |
| Gateway Rule | Pink #ec4899 |
Triangle | Firewall policies |
python main.py [OPTIONS]
Options:
--debug Enable verbose debug logging
--output, -o FILE Output HTML file (default: network_topology.html)
--no-browser Don't auto-open browser after generation
--no-devices Skip fetching WARP devices (faster)
--no-tunnel-configs Skip fetching detailed tunnel configurations
--include-gateway Include Gateway firewall rules
| Variable | Required | Description |
|---|---|---|
CLOUDFLARE_API_TOKEN |
✅* | API token with Zero Trust read permissions |
CLOUDFLARE_ACCOUNT_ID |
❌ | Auto-discovered from token; set only for multi-account tokens |
DEBUG |
❌ | Set to true for debug logging |
* Not needed if you use
wrangler login(Option A above).
cloudflare-topology/
├── main.py # CLI entry point
├── config.py # Configuration, colors, API endpoints
├── requirements.txt # Python dependencies
├── Dockerfile # Container image
├── Makefile # Build/run commands
│
├── models/
│ └── cloudflare_data.py # Data models for all resources
│
├── services/
│ ├── cloudflare_api.py # Cloudflare API client with pagination
│ ├── network_graph.py # Graph builder (nodes & edges)
│ └── renderer.py # HTML/CSS/JS visualization
│
├── server/
│ └── server.py # Flask server for Docker
│
└── tests/
├── test_config.py
├── test_models.py
└── test_network_graph.py
# Install dev dependencies
pip install -r requirements.txt
# Run tests
make test
# or
python -m pytest tests/ -v
# Lint
make lintContributions welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests
- Submit a pull request
Apache 2.0 - see LICENSE for details.
- tailscale-network-topology-mapper - Original inspiration
- Pyvis - Python network visualization
- vis.js - JavaScript graph library
- Cloudflare - Zero Trust platform
Made with ☁️ for the Cloudflare community
