Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Deploying Cloudflare Access with Cloudflare Tunnel to provide zero trust access to self-hosted and private applications, configuring identity-aware access policies, device posture checks, and WARP client enrollment for VPN replacement.
.claude/skills/deploying-cloudflare-access-for-zero-trust/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-09 | ✗→✓ | ▲ Improved | — | — |
| case-10 | ✗→✓ | ▲ Improved | — | — |
Do not use for applications requiring persistent UDP connections not supported by Cloudflare Tunnel, for environments requiring air-gapped or fully on-premises access control, or when regulatory requirements prohibit routing traffic through third-party cloud infrastructure.
cloudflared tunnel daemonInstall cloudflared and create a persistent tunnel to expose internal services.
bash# Install cloudflared on Ubuntu/Debian curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb \ -o cloudflared.deb sudo dpkg -i cloudflared.deb # Authenticate cloudflared with your Cloudflare account cloudflared tunnel login # Create a named tunnel cloudflared tunnel create internal-apps # Output: Created tunnel internal-apps with id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Configure tunnel routes to internal applications cat > ~/.cloudflared/config.yml << 'EOF' tunnel: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx credentials-file: /home/admin/.cloudflared/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json ingress: - hostname: wiki.company.com service: http://localhost:8080 - hostname: git.company.com service: http://10.1.1.50:3000 - hostname: grafana.company.com service: http://10.1.1.60:3000 - hostname: ssh.company.com service: ssh://localhost:22 - hostname: rdp.company.com service: rdp://10.1.1.100:3389 # Catch-all rule (required) - service: http_status:404 EOF # Route DNS to the tunnel cloudflared tunnel route dns internal-apps wiki.company.com cloudflared tunnel route dns internal-apps git.company.com cloudflared tunnel route dns internal-apps grafana.company.com # Run tunnel as a systemd service sudo cloudflared service install sudo systemctl enable cloudflared sudo systemctl start cloudflared # Verify tunnel status cloudflared tunnel info internal-apps
Set up authentication with your organization's identity provider.
bash# Using Cloudflare API to configure Okta as IdP curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/access/identity_providers" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "Corporate Okta", "type": "okta", "config": { "client_id": "OKTA_CLIENT_ID", "client_secret": "OKTA_CLIENT_SECRET", "okta_account": "company.okta.com", "api_token": "OKTA_API_TOKEN", "claims": ["email", "groups", "name"], "email_claim_name": "email" } }' # Configure Microsoft Entra ID as additional IdP curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/access/identity_providers" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "Microsoft Entra ID", "type": "azureAD", "config": { "client_id": "AZURE_APP_CLIENT_ID", "client_secret": "AZURE_APP_CLIENT_SECRET", "directory_id": "AZURE_TENANT_ID", "support_groups": true, "claims": ["email", "groups", "name"] } }'
Define Access applications with identity-aware policies for each internal service.
bash# Create Access application for internal wiki curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/access/apps" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "Internal Wiki", "domain": "wiki.company.com", "type": "self_hosted", "session_duration": "8h", "auto_redirect_to_identity": true, "http_only_cookie_attribute": true, "same_site_cookie_attribute": "lax", "logo_url": "https://company.com/wiki-logo.png", "allowed_idps": ["OKTA_IDP_ID", "AZURE_IDP_ID"] }' # Create Allow policy for the wiki application curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/access/apps/{app_id}/policies" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "Allow Engineering Team", "decision": "allow", "precedence": 1, "include": [ {"group": {"id": "ENGINEERING_GROUP_ID"}}, {"okta": {"name": "Engineering", "identity_provider_id": "OKTA_IDP_ID"}} ], "require": [ {"device_posture": {"integration_uid": "CROWDSTRIKE_INTEGRATION_ID"}} ] }' # Create Access application for SSH access curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/access/apps" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "SSH Access", "domain": "ssh.company.com", "type": "ssh", "session_duration": "4h", "auto_redirect_to_identity": true }'
Enroll corporate devices using Cloudflare WARP for private network access and device posture.
bash# Create device enrollment rule curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/devices/policy" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "Corporate Device Enrollment", "match": "identity.email matches \".*@company\\.com$\"", "precedence": 100, "enabled": true, "gateway_unique_id": "GATEWAY_ID", "support_url": "https://helpdesk.company.com/warp-help" }' # Install WARP on macOS via MDM (Jamf/Intune) # Download: https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/download-warp/ # Deploy with MDM configuration profile: cat > warp_mdm_config.plist << 'EOF' <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>organization</key> <string>company</string> <key>auto_connect</key> <integer>1</integer> <key>switch_locked</key> <true/> <key>onboarding</key> <false/> </dict> </plist> EOF # Install Cloudflare root certificate for TLS inspection # Download from: https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/user-side-certificates/ sudo cp cloudflare-root-ca.pem /usr/local/share/ca-certificates/cloudflare-root-ca.crt sudo update-ca-certificates # Configure split tunnel to route private network through WARP curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/devices/policy/{policy_id}/fallback_domains" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '[ {"suffix": "internal.corp", "description": "Internal corporate domain"}, {"suffix": "10.0.0.0/8", "description": "Private network range"} ]'
Integrate endpoint security signals into Access policies.
bash# Add CrowdStrike device posture integration curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/devices/posture/integration" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "CrowdStrike Falcon", "type": "crowdstrike_s2s", "config": { "api_url": "https://api.crowdstrike.com", "client_id": "CS_API_CLIENT_ID", "client_secret": "CS_API_CLIENT_SECRET", "customer_id": "CS_CUSTOMER_ID" }, "interval": "10m" }' # Create device posture rule for disk encryption curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/devices/posture" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "Disk Encryption Required", "type": "disk_encryption", "match": [{"platform": "windows"}, {"platform": "mac"}], "input": {"requireAll": true} }' # Create device posture rule for OS version curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/devices/posture" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "Minimum OS Version", "type": "os_version", "match": [{"platform": "windows"}], "input": {"version": "10.0.19045", "operator": ">="} }'
Configure logging for access decisions and tunnel health monitoring.
bash# Enable Logpush for Access audit logs to S3 curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/logpush/jobs" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "name": "access-audit-logs", "output_options": { "field_names": ["RayID","Action","Allowed","AppDomain","AppUUID","Connection","Country","CreatedAt","Email","IPAddress","PurposeJustificationPrompt","PurposeJustificationResponse","TemporaryAccessDuration","UserUID"], "timestamp_format": "rfc3339" }, "destination_conf": "s3://security-logs-bucket/cloudflare-access/?region=us-east-1&access-key-id=AKID&secret-access-key=SECRET", "dataset": "access_requests", "enabled": true }' # Query access logs via GraphQL Analytics API curl -X POST "https://api.cloudflare.com/client/v4/graphql" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{ "query": "{ viewer { accounts(filter: {accountTag: \"ACCOUNT_ID\"}) { accessLoginRequestsAdaptiveGroups(filter: {datetime_gt: \"2026-02-22T00:00:00Z\"}, limit: 100, orderBy: [count_DESC]) { dimensions { action appName userEmail country } count } } } }" }'
| Term | Definition | |------|------------| | Cloudflare Tunnel | Encrypted outbound-only connection from your infrastructure to Cloudflare's network, exposing internal services without opening inbound firewall ports | | Cloudflare Access | Identity-aware reverse proxy evaluating every request against access policies before granting access to protected applications | | WARP Client | Cloudflare's endpoint agent that routes device traffic through Cloudflare's network for policy enforcement and private network access | | Access Application | Configuration object defining a protected resource (self-hosted, SaaS, or infrastructure) with associated access policies | | Device Posture | Endpoint health signals (OS version, disk encryption, EDR status) evaluated as conditions in Access policies | | Cloudflare One | Unified SASE platform combining ZTNA (Access), SWG (Gateway), CASB, DLP, and RBI |
Context: A SaaS startup with 200 employees and no existing VPN wants to provide secure access to internal tools (Grafana, internal APIs, staging environments) running on AWS. Budget is limited, and the team has no dedicated security staff.
Approach:
cloudflared tunnel on an EC2 instance in the production VPCPitfalls: Cloudflare root certificate must be installed on all devices for TLS inspection to work; some applications may break with TLS interception. Tunnel failover requires running multiple cloudflared instances or using Cloudflare's replicas feature. Access policies should always include a default deny rule. WebSocket applications may require specific tunnel configuration.
Cloudflare Zero Trust Deployment Report
==================================================
Organization: StartupCorp
Team Name: startupcorp
Deployment Date: 2026-02-23
TUNNEL INFRASTRUCTURE:
Active Tunnels: 2 (primary + failover)
Tunnel Status: Healthy
Connected Edge: Washington DC, Ashburn
Ingress Routes: 8
ACCESS APPLICATIONS:
Self-Hosted Apps: 6
SaaS Apps: 3
SSH/Infrastructure: 2
Total Policies: 15
DEVICE ENROLLMENT:
Enrolled Devices: 187 / 200
WARP Connected: 182 / 187 (97.3%)
Posture Compliant: 175 / 187 (93.6%)
ACCESS METRICS (last 30 days):
Total Requests: 89,432
Allowed: 88,756 (99.2%)
Blocked: 676 (0.8%)
Unique Users: 195
Countries: 12
Avg Session Duration: 6.2 hours| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-24 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +38 percentage points is the difference between those two pass rates over the 24 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.