cloudflare-zero-trust/SKILL.md
Use when working with Cloudflare Tunnel or Access - tunnel setup, authentication configuration, 502 Bad Gateway errors, Docker/Kubernetes deployment, service token management, private network routing (SSH/RDP/databases), WebSocket/gRPC connection issues, replica scaling problems, WARP routing, Terraform/IaC automation, local development with quick tunnels, audit logging setup, compliance requirements (SOC2/HIPAA), or advanced network debugging. Keywords - cloudflared, 502 error, service tokens, terraform, metrics port 20241, trycloudflare, Logpush, SIEM. CRITICAL - Authentication mandatory not optional.
npx skillsauth add acedergren/agentic-tools cloudflare-zero-trustInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Cloudflare Zero Trust provides secure remote access to applications without VPN, using Cloudflare Tunnel (secure connectivity) and Cloudflare Access (authentication/authorization).
Core principle: Authentication is not optional. Every tunnel must have access controls from day one.
Use this skill when:
Symptoms that trigger this skill:
UNAUTHENTICATED TUNNEL = PUBLICLY EXPOSED SERVICE = SECURITY INCIDENT
Authentication is mandatory, not optional.
If you find yourself thinking:
STOP. These are security violations.
Correct mindset:
| Task | Remotely-Managed (Dashboard) | Locally-Managed (config.yml) |
|------|------------------------------|------------------------------|
| Best for | GUI users, quick setup, team collaboration | IaC, automation, version control, CI/CD |
| Setup | Dashboard → Create Tunnel → Configure routes | cloudflared tunnel create + config.yml |
| Changes | Click to update | Edit config, restart service |
| Access Control | Always via dashboard (Zero Trust → Access) | Tunnel via config, Access via dashboard |
| Docker | Env vars for credentials | Mount config.yml + credentials JSON |
BEFORE creating tunnel, choose management approach using the decision tree in "Dashboard vs CLI Decision Tree" section below.
digraph tunnel_setup {
rankdir=TD;
node [shape=box, style=rounded];
start [label="Need to expose app", shape=ellipse];
requirements [label="Define requirements\n• Who needs access?\n• Auth provider?\n• Docker or host?"];
choose_method [label="Choose management", shape=diamond];
dashboard [label="Dashboard Method\n1. Create tunnel\n2. Install cloudflared\n3. Run connector"];
config [label="Config File Method\n1. cloudflared tunnel create\n2. Write config.yml\n3. Start service"];
access [label="Configure Access\nBEFORE going live", style="filled", fillcolor="#ffcccc"];
test [label="Test authentication"];
deploy [label="Enable for users"];
monitor [label="Monitor logs"];
start -> requirements;
requirements -> choose_method;
choose_method -> dashboard [label="GUI\nQuick start"];
choose_method -> config [label="IaC\nAutomation"];
dashboard -> access;
config -> access;
access -> test;
test -> deploy;
deploy -> monitor;
}
Critical: Configure Access authentication BEFORE exposing the tunnel. Never deploy unauthenticated tunnels, even "temporarily."
When to use:
Setup:
prod-api-tunnelcloudflared service install <TOKEN>
api.example.comhttp://localhost:8080Advantages:
Disadvantages:
When to use:
Setup:
Create tunnel:
cloudflared tunnel create myapp-tunnel
Outputs: ~/.cloudflared/<TUNNEL_ID>.json (credentials)
Create config file (~/.cloudflared/config.yml):
# === REQUIRED FIELDS (tunnel breaks without these) ===
tunnel: <TUNNEL_ID>
credentials-file: /etc/cloudflared/<TUNNEL_ID>.json
ingress:
# Route myapp.example.com to local service
- hostname: myapp.example.com
service: http://localhost:8080 # REQUIRED: origin URL
originRequest:
# === DANGEROUS (wrong value = security risk or outage) ===
noTLSVerify: false # NEVER set true without specific reason (security risk)
# === SAFE (tune based on your needs) ===
connectTimeout: 30s
httpHostHeader: myapp.example.com
# Multiple services example
- hostname: api.example.com
service: http://localhost:8080
- hostname: admin.example.com
service: http://localhost:3000
# REQUIRED: Catch-all rule (must be last)
- service: http_status:404
# === SAFE (change freely) ===
loglevel: info # or: debug, warn, error
Route DNS:
cloudflared tunnel route dns myapp-tunnel myapp.example.com
Run tunnel:
# Foreground (testing)
cloudflared tunnel run myapp-tunnel
# As service (production)
sudo cloudflared service install
sudo systemctl start cloudflared
sudo systemctl enable cloudflared
Configure Access (see next section)
Advantages:
Disadvantages:
MANDATORY when working with WebSockets, HTTP/2, or gRPC:
Load protocols.md for complete protocol configuration, tuning parameters, and troubleshooting.
Quick reference:
keepAliveTimeout: 300s for stable connectionshttp2Origin: trueDo NOT load for basic HTTP tunnel setup.
When to use:
Critical networking difference (Docker/Kubernetes):
# ❌ WRONG - localhost doesn't resolve in containers
ingress:
- hostname: myapp.example.com
service: http://localhost:8080
# ✅ RIGHT - use service/pod name
# Docker Compose:
service: http://myapp:8080 # Container name from docker-compose.yml
# Kubernetes:
service: http://myapp-service:8080 # Service name
Credential mounting patterns:
# Docker Compose:
volumes:
- ./cloudflared/config.yml:/etc/cloudflared/config.yml:ro # Read-only
- ./cloudflared/credentials.json:/etc/cloudflared/credentials.json:ro
# Kubernetes:
# Use ConfigMap for config, Secret for credentials
Health check setup:
# Docker Compose:
healthcheck:
test: ["CMD", "cloudflared", "tunnel", "info"]
interval: 30s
timeout: 10s
retries: 3
# Kubernetes:
livenessProbe:
exec:
command: ["cloudflared", "tunnel", "info"]
initialDelaySeconds: 30
periodSeconds: 30
Redundancy for production:
# Docker Compose - use multiple instances behind load balancer
# Kubernetes - use replicas:
spec:
replicas: 2 # Minimum 2 for zero-downtime updates
Setup workflow:
cloudflared tunnel create myapp-tunnel~/.cloudflared/<TUNNEL_ID>.jsonVerification:
# Docker Compose
docker-compose logs cloudflared
docker-compose exec cloudflared cloudflared tunnel info
# Kubernetes
kubectl logs -l app=cloudflared
kubectl exec deploy/cloudflared -- cloudflared tunnel info
MANDATORY when exposing SSH, RDP, databases, or custom TCP/UDP protocols:
Load private-networks.md for complete private network configuration, WARP routing setup, container networking, and access policies.
Quick reference:
service: ssh://192.168.1.10:22service: rdp://192.168.1.20:3389service: tcp://192.168.1.30:5432tcp://database:5432 NOT tcp://localhost:5432Do NOT load for HTTP application tunnels.
Configure Access BEFORE exposing tunnel to users.
Zero Trust → Access → Applications → Add an application
Choose "Self-hosted"
Application Configuration:
My Application24 hours (default)myapp.example.comAdd policies (at least one Allow policy required):
Example policies:
Email-based:
Name: Allow specific users
Action: Allow
Include: Emails → [email protected], [email protected]
Domain-based:
Name: Allow company domain
Action: Allow
Include: Email domains → @company.com
Group-based (requires IdP groups):
Name: Allow admins group
Action: Allow
Include: Azure AD Groups → Admins
Multi-factor authentication:
Name: Require MFA for admins
Action: Allow
Include: Email domains → @company.com
Require: Authentication Method → mTLS, WARP, Service Token (select MFA method)
For integrated SaaS apps (Salesforce, Workday, etc.):
Azure AD setup:
Cloudflare Accesshttps://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callbackCloudflare configuration:
Azure AD<Application (client) ID><Secret value>Use in policies:
Name: Allow Admins group
Action: Allow
Include: Azure AD Groups → select "Admins" group
Cloudflare Accesshttps://<team-name>.cloudflareaccess.com/cdn-cgi/access/callbackCloudflare:
https://your-domain.okta.comFor providers not in catalog:
https://provider.com/oauth2/authorizehttps://provider.com/oauth2/tokenhttps://provider.com/.well-known/jwks.jsonPrinciple: Least privilege by default.
Policy evaluation order:
Good policy patterns:
Production application with role-based access:
Policy 1: Block non-employees
Action: Block
Include: Everyone
Exclude: Email domains → @company.com
Policy 2: Allow developers
Action: Allow
Include: Azure AD Groups → Developers
Policy 3: Allow admins
Action: Allow
Include: Azure AD Groups → Admins
Require: MFA
Staging environment:
Policy 1: Allow dev team
Action: Allow
Include: Email domains → @company.com
Require: Azure AD Groups → Developers OR Admins
API with service authentication:
Policy 1: Service-to-service
Action: Service Auth
Include: Service Token → api-service-token
MANDATORY when configuring CI/CD pipelines, microservices, or any non-human authentication:
Load service-auth.md for service token creation, rotation workflows, security best practices, and M2M patterns.
Quick reference:
CF-Access-Client-Id, CF-Access-Client-SecretDo NOT load for user authentication (use SSO/OIDC instead).
Error: 502 Bad Gateway - Unable to reach the origin service
Meaning: Tunnel is connected to Cloudflare, but can't reach your origin.
Systematic diagnosis:
digraph troubleshoot_502 {
rankdir=TD;
node [shape=box, style=rounded];
start [label="502 Bad Gateway", shape=ellipse, style=filled, fillcolor="#ffcccc"];
check_origin [label="Can you curl origin\nlocally?", shape=diamond];
origin_down [label="Origin service down\nRestart service", style=filled, fillcolor="#ffcccc"];
check_docker [label="Is cloudflared\nin Docker?", shape=diamond];
wrong_service [label="Using localhost?\nChange to container name:\nhttp://myapp:8080", style=filled, fillcolor="#ccffcc"];
check_tls [label="Origin uses\nHTTPS?", shape=diamond];
tls_issue [label="Self-signed cert?\nSet noTLSVerify: true\nOR add caPool", style=filled, fillcolor="#ccffcc"];
check_port [label="Port correct\nin config?", shape=diamond];
wrong_port [label="Fix port number\nin config.yml", style=filled, fillcolor="#ccffcc"];
firewall [label="Check firewall\nrules/logs", style=filled, fillcolor="#ffffcc"];
start -> check_origin;
check_origin -> origin_down [label="No"];
check_origin -> check_docker [label="Yes"];
check_docker -> wrong_service [label="Yes"];
check_docker -> check_tls [label="No"];
check_tls -> tls_issue [label="Yes"];
check_tls -> check_port [label="No"];
check_port -> wrong_port [label="No"];
check_port -> firewall [label="Yes"];
}
Quick diagnostic commands:
# 1. Test origin locally
curl -v http://localhost:8080
# Success? Continue. Connection refused? Origin is down.
# 2. Check cloudflared logs
sudo journalctl -u cloudflared -n 50 --no-pager # Systemd
docker logs cloudflared # Docker
# Look for: "dial tcp", "connection refused", "context deadline exceeded"
# 3. Test from cloudflared container (if Docker)
docker exec cloudflared curl http://myapp:8080
# Fails? Wrong service name or network issue
# 4. Run tunnel in foreground (see real-time errors)
sudo systemctl stop cloudflared
cloudflared tunnel run myapp-tunnel
Symptom: Browser keeps redirecting to login, never reaches app
Systematic diagnosis:
digraph troubleshoot_auth_loop {
rankdir=TD;
node [shape=box, style=rounded];
start [label="Authentication Loop", shape=ellipse, style=filled, fillcolor="#ffcccc"];
check_policy [label="Does an Allow policy\nmatch your user?", shape=diamond];
policy_issue [label="Add/update Allow policy\nCheck email/groups match", style=filled, fillcolor="#ccffcc"];
check_idp [label="IdP redirect URI\nmatches Cloudflare?", shape=diamond];
idp_issue [label="Fix redirect URI:\nhttps://<team>.cloudflareaccess.com\n/cdn-cgi/access/callback", style=filled, fillcolor="#ccffcc"];
check_groups [label="Using group-based\npolicies?", shape=diamond];
groups_issue [label="Verify IdP sends group claims\nCheck 'Support groups' enabled", style=filled, fillcolor="#ccffcc"];
check_cookies [label="Try incognito mode\nClear all cookies", shape=diamond];
cookie_issue [label="Browser blocking 3rd-party\ncookies or privacy mode active", style=filled, fillcolor="#ffffcc"];
check_session [label="Session duration\nadequate?", shape=diamond];
session_issue [label="Increase session duration\nDefault 24h may be too short", style=filled, fillcolor="#ccffcc"];
escalate [label="Check Cloudflare Access logs\nfor specific error", style=filled, fillcolor="#ffffcc"];
start -> check_policy;
check_policy -> policy_issue [label="No"];
check_policy -> check_idp [label="Yes"];
check_idp -> idp_issue [label="No"];
check_idp -> check_groups [label="Yes"];
check_groups -> groups_issue [label="Yes"];
check_groups -> check_cookies [label="No"];
check_cookies -> cookie_issue [label="Still loops"];
check_cookies -> check_session [label="Works in\nincognito"];
check_session -> session_issue [label="No"];
check_session -> escalate [label="Yes"];
}
Quick diagnostic steps:
# 1. Verify Access policy matches you
# Dashboard → Zero Trust → Access → Applications → <Your App> → Policies
# Check: Does an Allow policy include your email/domain/group?
# 2. Test in incognito window (eliminates cookie issues)
# If works in incognito → Clear cookies
# If still loops → Policy or IdP issue
# 3. Check IdP redirect URI
# Must exactly match: https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback
# 4. Verify group claims (if using group-based policies)
# Azure AD: Token configuration → Add groups claim
# Okta: Profile → Edit → Include in token
# Cloudflare: Settings → Authentication → <IdP> → Support groups ✓
# 5. Check Access logs for specific error
# Dashboard → Zero Trust → Logs → Access
# Look for: policy evaluation failures, IdP errors
Symptom: Tunnel shows "Inactive" or "Down" in dashboard
Troubleshooting:
Check cloudflared is running:
sudo systemctl status cloudflared # or: docker ps | grep cloudflared
Verify credentials file exists:
ls -la ~/.cloudflared/<TUNNEL_ID>.json
Firewall requirement: Cloudflared needs outbound HTTPS (443) to *.argotunnel.com
Restart tunnel:
sudo systemctl restart cloudflared
Symptom: nslookup myapp.example.com returns no records
Fix:
Check DNS record exists:
myapp.example.com → <TUNNEL_ID>.cfargotunnel.comCreate if missing:
cloudflared tunnel route dns myapp-tunnel myapp.example.com
Verify: DNS record should be "Proxied" (orange cloud) in Cloudflare dashboard
MANDATORY when basic troubleshooting fails or for performance debugging:
Load advanced-troubleshooting.md for network path analysis, performance tuning, certificate debugging, connection limits, and metrics interpretation.
Quick reference:
Do NOT load for simple 502 or DNS issues.
❌ Wrong:
# Create tunnel
cloudflared tunnel create myapp
# Route DNS
cloudflared tunnel route dns myapp myapp.example.com
# Run tunnel
cloudflared tunnel run myapp
# ⚠️ APP IS NOW PUBLICLY ACCESSIBLE
✅ Right:
# Create tunnel
cloudflared tunnel create myapp
# Route DNS
cloudflared tunnel route dns myapp myapp.example.com
# Configure Access FIRST (dashboard)
# THEN run tunnel
cloudflared tunnel run myapp
noTLSVerify: true Without Reason❌ Wrong:
originRequest:
noTLSVerify: true # "Just in case"
✅ Right:
# Only if origin uses self-signed cert
originRequest:
noTLSVerify: false # Verify by default
# OR if self-signed:
# noTLSVerify: true
# caPool: /path/to/custom-ca.pem
❌ Wrong:
# config.yml
ingress:
- hostname: app.example.com
service: http://localhost:8080 # Won't work in Docker!
✅ Right:
# config.yml
ingress:
- hostname: app.example.com
service: http://myapp:8080 # Use container name
❌ Wrong:
Policy: Skip auth for internal
Action: Bypass
Include: Everyone
# ⚠️ Anyone with URL can access!
✅ Right:
Policy: Allow employees
Action: Allow
Include: Email domains → @company.com
❌ Wrong:
# config.yml checked into git
tunnel: abc123-def456-...
credentials-file: /etc/cloudflared/credentials.json
# ⚠️ credentials.json also in git!
✅ Right:
# .gitignore
*.json
credentials.json
Use environment-specific credentials, never commit.
digraph decision {
rankdir=TD;
node [shape=box, style=rounded];
start [label="Need tunnel", shape=ellipse];
team [label="Team has IaC\nexperience?", shape=diamond];
automation [label="Need automation\nor CI/CD?", shape=diamond];
multi_env [label="Multiple\nenvironments?", shape=diamond];
cli [label="Use config.yml\n(Locally-managed)", style="filled", fillcolor="#ccffcc"];
dashboard [label="Use Dashboard\n(Remotely-managed)", style="filled", fillcolor="#ccccff"];
start -> team;
team -> automation [label="Yes"];
team -> dashboard [label="No"];
automation -> cli [label="Yes"];
automation -> multi_env [label="No"];
multi_env -> cli [label="Yes"];
multi_env -> dashboard [label="No"];
}
Note: Browser automation for dashboard management requires MCP claude-in-chrome tools.
Use cases:
Pattern:
// Pseudocode - requires claude-in-chrome MCP
const { navigate, click, fill } = mcp_claude_in_chrome;
// Navigate to Zero Trust dashboard
await navigate('https://one.dash.cloudflare.com/');
// Login flow (use saved session or credentials)
// ...
// Create tunnel
await navigate('Networks/Tunnels');
await click('Create a tunnel');
await fill('Tunnel name', 'new-tunnel');
await click('Save tunnel');
// Configure route
await click('Public Hostname');
await fill('Subdomain', 'myapp');
await fill('Domain', 'example.com');
await fill('Service', 'http://localhost:8080');
await click('Save');
Better alternative: Use Cloudflare API
# Get tunnels
curl -X GET "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel" \
-H "Authorization: Bearer ${API_TOKEN}"
# Create tunnel
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"name": "new-tunnel",
"tunnel_secret": "<base64-secret>"
}'
# Update config
curl -X PUT \
"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}/configurations" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"config": {
"ingress": [
{
"hostname": "myapp.example.com",
"service": "http://localhost:8080"
},
{
"service": "http_status:404"
}
]
}
}'
When to use browser automation vs API:
MANDATORY when automating via Cloudflare API, CI/CD pipelines, or GitOps:
Load api-automation.md for complete API reference including Access applications, policies, tunnel management, service tokens, IdP configuration, device posture, and analytics.
Quick reference:
https://api.cloudflare.com/client/v4/Authorization: Bearer ${API_TOKEN}Do NOT load if using dashboard or Terraform (use terraform.md instead).
MANDATORY when using quick tunnels, preview environments, or local dev setups:
Load local-development.md for quick tunnel patterns (trycloudflare.com), preview environments, hot reload setups, and webhook testing.
Quick reference:
cloudflared tunnel --url http://localhost:8080Do NOT load for production deployments.
MANDATORY when managing Cloudflare with Terraform or other IaC tools:
Load terraform.md for complete provider setup, resource examples, multi-environment patterns, state management, and deployment workflows.
Quick reference:
cloudflare/cloudflare v4.0+cloudflare_tunnel, cloudflare_access_application, cloudflare_access_policyDo NOT load for dashboard-based management.
MANDATORY for compliance requirements (SOC2, HIPAA, ISO 27001) or SIEM integration:
Load audit-logging.md for Logpush configuration, SIEM patterns, compliance checklists, log retention, and alerting strategies.
Quick reference:
Do NOT load for basic tunnel setup.
Stop and reconsider if you find yourself:
noTLSVerify: true without specific reasonAll of these are security incidents, not acceptable tradeoffs.
# Create tunnel
cloudflared tunnel create <NAME>
# List tunnels
cloudflared tunnel list
# Run tunnel (foreground)
cloudflared tunnel run <NAME>
# Route DNS
cloudflared tunnel route dns <NAME> <HOSTNAME>
# Install as service
sudo cloudflared service install
# Service management
sudo systemctl start cloudflared
sudo systemctl stop cloudflared
sudo systemctl restart cloudflared
sudo systemctl status cloudflared
# Check logs
sudo journalctl -u cloudflared -f
# Tunnel info
cloudflared tunnel info <NAME>
# Delete tunnel
cloudflared tunnel delete <NAME>
# Update cloudflared
# macOS
brew upgrade cloudflare/cloudflare/cloudflared
# Linux
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
This skill is based on:
development
--- name: api-audit description: "Use when auditing API routes for schema drift, missing auth, or validation gaps. Scans routes against shared TypeScript types to find mismatches, missing middleware, and undocumented endpoints. Read-only — produces a severity-grouped report. Keywords: audit routes, schema drift, auth gaps, missing validation, type mismatch, orphaned schemas. Triggers on "audit API routes" or "find schema drift"." --- # API Route & Type Audit Skill ## When to Use Load this skil
development
Use when drafting, translating, polishing, or reviewing Swedish text so it sounds natural, fluent, contemporary, and appropriate for its audience. Triggers include "write better Swedish", "make this sound natural in Swedish", "translate into Swedish", "polish this Swedish", "tech company Swedish", "contemporary Swedish words", "Swedish developer docs", and "avoid Anglicisms".
development
Use when working with shadcn-svelte components, TanStack Table in Svelte 5, or Tailwind v4.1. Covers non-obvious reactivity bugs, library selection trade-offs, and migration pitfalls not in the official docs. Keywords: shadcn-svelte, TanStack Table, Tailwind v4.1, Svelte 5 runes, bits-ui, superforms, data table, svelte-check.
data-ai
Use when mapping IDCS claims to org membership after OAuth login succeeds. Covers mapProfileToUser, session.create.before, session.create.after hooks, MERGE INTO upserts, tenant-org mapping, and first-admin bootstrap. Keywords: IDCS groups, org_members, provisioning, session hooks, tenant map, MERGE INTO.