Cloudflare Zero Trust Hardening Guide
Security hardening for Cloudflare Zero Trust, Access, Gateway, and WARP deployment
Overview
Cloudflare Zero Trust is a comprehensive security platform providing secure access to applications, DNS filtering, and endpoint protection. With billions of DNS queries processed daily and protection for millions of users, Cloudflare’s Zero Trust services are critical infrastructure for modern security architectures. This guide covers hardening Access (ZTNA), Gateway (SWG/CASB), and WARP (endpoint agent).
Intended Audience
- Security engineers managing Cloudflare Zero Trust deployments
- IT administrators configuring access policies
- GRC professionals assessing Zero Trust compliance
- Third-party risk managers evaluating security tools
How to Use This Guide
- L1 (Crawl): Essential controls for all organizations
- L2 (Walk): Enhanced controls for security-sensitive environments
- L3 (Run): Strictest controls for regulated industries
Scope
This guide covers Cloudflare Zero Trust components including Access, Gateway, WARP client, and Tunnel configurations. CDN and DDoS protection are covered in separate guides.
Table of Contents
- Authentication & Access Controls
- Access Application Policies
- Gateway Security Policies
- WARP Client Hardening
- Tunnel Security
- Monitoring & Detection
- Compliance Quick Reference
1. Authentication & Access Controls
1.1 Configure Identity Provider Integration
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.3, 12.5 |
| NIST 800-53 | IA-2, IA-8 |
Description
Integrate Cloudflare Zero Trust with your corporate identity provider to enable SSO authentication for Access applications and WARP enrollment.
Rationale
Why This Matters:
- Centralizes authentication management
- Enables MFA through your IdP
- Provides consistent identity across all Zero Trust services
- Enables user and group-based policies
Attack Prevented: Account takeover via fragmented authentication, access without IdP-enforced MFA
Prerequisites
- Cloudflare Zero Trust account
- Identity provider with OIDC or SAML support
- Admin access to Zero Trust dashboard
ClickOps Implementation
Step 1: Add Identity Provider
- Navigate to: Zero Trust Dashboard → Settings → Authentication
- Click Add new
- Select your IdP type:
- Okta, Azure AD, OneLogin: Use preconfigured templates
- Generic OIDC/SAML: Manual configuration
- Configure IdP settings:
- Client ID/Secret: From IdP application
- Authorization URL: IdP OAuth endpoint
- Token URL: IdP token endpoint
Step 2: Configure IdP (Example: Okta)
- In Okta Admin: Applications → Create App Integration
- Select OIDC - Web Application
- Configure:
- Sign-in redirect:
https://<team-name>.cloudflareaccess.com/cdn-cgi/access/callback - Sign-out redirect:
https://<team-name>.cloudflareaccess.com
- Sign-in redirect:
- Assign users/groups
- Copy Client ID and Secret to Cloudflare
Step 3: Test Authentication
- In Cloudflare, click Test on IdP configuration
- Verify successful authentication
- Enable the IdP for production use
Time to Complete: ~45 minutes
Code Pack: Terraform
resource "cloudflare_zero_trust_access_identity_provider" "corporate_idp" {
account_id = var.cloudflare_account_id
name = "Corporate IdP"
type = "oidc"
config = {
client_id = var.oidc_client_id
client_secret = var.oidc_client_secret
auth_url = var.oidc_auth_url
token_url = var.oidc_token_url
certs_url = var.oidc_certs_url
claims = ["email_verified", "preferred_username", "groups"]
scopes = ["openid", "email", "profile", "groups"]
}
}
Code Pack: API Script
# Add OIDC identity provider to Zero Trust
info "1.1 Adding OIDC identity provider..."
: "${CF_IDP_CLIENT_ID:?Set CF_IDP_CLIENT_ID}"
: "${CF_IDP_CLIENT_SECRET:?Set CF_IDP_CLIENT_SECRET}"
: "${CF_IDP_AUTH_URL:?Set CF_IDP_AUTH_URL}"
: "${CF_IDP_TOKEN_URL:?Set CF_IDP_TOKEN_URL}"
RESPONSE=$(cf_post "/accounts/${CF_ACCOUNT_ID}/access/identity_providers" "{
\"name\": \"Corporate IdP\",
\"type\": \"oidc\",
\"config\": {
\"client_id\": \"${CF_IDP_CLIENT_ID}\",
\"client_secret\": \"${CF_IDP_CLIENT_SECRET}\",
\"auth_url\": \"${CF_IDP_AUTH_URL}\",
\"token_url\": \"${CF_IDP_TOKEN_URL}\",
\"claims\": [\"email_verified\", \"preferred_username\", \"groups\"],
\"scopes\": [\"openid\", \"email\", \"profile\", \"groups\"]
}
}") || {
fail "1.1 Failed to add identity provider"
increment_failed
summary
exit 0
}
Code Pack: Sigma Detection Rule
detection:
selection:
ActionType|contains:
- 'DeleteAccessIdentityProvider'
- 'UpdateAccessIdentityProvider'
condition: selection
fields:
- ActorEmail
- ActionType
- ResourceID
- When
1.2 Configure Multi-Factor Authentication
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.5 |
| NIST 800-53 | IA-2(1) |
Description
Ensure MFA is enforced for all Access application authentications through IdP policies or Cloudflare’s additional MFA requirements.
Rationale
Why This Matters:
- Passwords alone are routinely defeated by phishing, credential stuffing, and reuse — MFA adds a second factor an attacker is far less likely to possess
- Cloudflare Access sits in front of internal and SaaS applications, so a single bypassed login can expose every protected resource
- Enforcing MFA at the IdP or in the Access policy guarantees the requirement applies to every authentication, not just the logins users choose to protect
- Phishing-resistant factors (FIDO2/WebAuthn) defeat real-time relay attacks that one-time codes cannot stop
Attack Prevented: Credential theft, phishing, credential stuffing, password reuse, account takeover
ClickOps Implementation
Option A: Enforce MFA via IdP (Recommended)
- Configure MFA requirement in your identity provider
- Create IdP policy requiring MFA for Cloudflare application
- All Access authentications will require MFA
Option B: Cloudflare Access Policy Requirement
- In Access application policy, add requirement:
- Rule type: Require
- Selector: Login Methods
- Value: Select IdPs with MFA configured
- Optionally add additional authentication factor via policy
Code Pack: Terraform
resource "cloudflare_zero_trust_access_policy" "require_mfa" {
account_id = var.cloudflare_account_id
name = "Require MFA for all users"
decision = "allow"
include = [{
email_domain = {
domain = var.corporate_domain
}
}]
require = [{
auth_method = {
auth_method = "mfa"
}
}]
session_duration = "24h"
}
Code Pack: API Script
# Verify MFA is required in Access policies
# MFA enforcement is set via Access policy 'require' rules
# Check each app for auth_method = mfa in require blocks
MFA_MISSING=0
while IFS= read -r app_id; do
APP_POLICIES=$(cf_get "/accounts/${CF_ACCOUNT_ID}/access/apps/${app_id}/policies") || continue
HAS_MFA=$(echo "${APP_POLICIES}" | jq '[.result[].require[]? | select(.auth_method.auth_method == "mfa")] | length')
if [ "${HAS_MFA}" = "0" ]; then
APP_NAME=$(echo "${POLICIES}" | jq -r ".result[] | select(.id == \"${app_id}\") | .name")
warn "1.2 Application '${APP_NAME}' does not require MFA"
MFA_MISSING=$((MFA_MISSING + 1))
fi
done < <(echo "${POLICIES}" | jq -r '.result[].id')
1.3 Harden Device Enrollment
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 1.4, 5.3 |
| NIST 800-53 | AC-2 |
Description
Configure device enrollment policies to control which devices can enroll in WARP and access your Zero Trust network.
Rationale
Why This Matters:
- Once enrolled, devices join your Zero Trust network
- Uncontrolled enrollment creates security risk
- Enrollment policies prevent unauthorized device access
Attack Prevented: Unauthorized or rogue device enrollment into the Zero Trust network
ClickOps Implementation
Step 1: Configure Enrollment Policies
- Navigate to: Settings → WARP Client → Device enrollment permissions
- Click Manage → Add a rule
- Configure enrollment restrictions:
- Emails ending in: @yourdomain.com
- Identity provider groups: Specific groups only
- Country: Allowed countries only
Step 2: Require IdP Authentication
- In enrollment rule, require authentication via IdP
- Add additional conditions:
- Specific IdP login method (e.g., Okta with MFA)
- Geographic restrictions
- Save rule
Time to Complete: ~20 minutes
Code Pack: Terraform
resource "cloudflare_zero_trust_access_application" "warp_enrollment" {
account_id = var.cloudflare_account_id
name = "Device Enrollment"
type = "warp"
session_duration = "24h"
allowed_idps = [cloudflare_zero_trust_access_identity_provider.corporate_idp.id]
auto_redirect_to_identity = true
}
resource "cloudflare_zero_trust_access_policy" "device_enrollment_policy" {
account_id = var.cloudflare_account_id
name = "Restrict device enrollment to corporate users"
decision = "allow"
include = [{
email_domain = {
domain = var.corporate_domain
}
}]
require = [{
auth_method = {
auth_method = "mfa"
}
}]
}
Code Pack: API Script
# Check device enrollment permissions
ENROLLMENT=$(cf_get "/accounts/${CF_ACCOUNT_ID}/devices/policy") || {
fail "1.3 Unable to retrieve device enrollment policy"
increment_failed
summary
exit 0
}
# Verify enrollment requires authentication
REQUIRE_AUTH=$(echo "${ENROLLMENT}" | jq -r '.result.allow_mode_switch // false')
info "1.3 Device policy retrieved -- reviewing enrollment settings"
# List enrollment rules
RULES=$(cf_get "/accounts/${CF_ACCOUNT_ID}/devices/policy/include") 2>/dev/null || true
EXCLUDE=$(cf_get "/accounts/${CF_ACCOUNT_ID}/devices/policy/exclude") 2>/dev/null || true
1.4 Configure Admin Role Restrictions
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 5.4 |
| NIST 800-53 | AC-6(1) |
Description
Configure granular admin roles in Cloudflare to limit dashboard access based on job responsibilities.
Rationale
Why This Matters:
- Super Administrator access grants full control over Zero Trust policies, DNS, and account settings — a compromised admin account can disable every protection at once
- Assigning least-privilege roles limits the blast radius if any single admin credential is phished or stolen
- Scoped roles such as Zero Trust Admin and Audit Log Viewer let teams do their jobs without holding billing or account-wide change rights
- Fewer privileged accounts means a smaller, more defensible attack surface for adversaries to target
Attack Prevented: Privilege escalation, insider misuse, account takeover, unauthorized configuration change
ClickOps Implementation
Step 1: Review Member Access
- Navigate to: Cloudflare Dashboard → Manage Account → Members
- Review current member roles
- Document Super Administrator assignments
Step 2: Implement Least Privilege
- Available roles:
- Super Administrator: Full access (limit to 2-3)
- Administrator: Most settings, no billing
- Zero Trust Admin: Zero Trust only
- Audit Log Viewer: Read-only logs
- Assign appropriate roles per responsibility
- Remove unnecessary Super Administrator access
Code Pack: Terraform
data "cloudflare_account_roles" "all" {
account_id = var.cloudflare_account_id
}
locals {
roles_by_name = {
for role in data.cloudflare_account_roles.all.result :
role.name => role
}
}
resource "cloudflare_account_member" "zt_admin" {
account_id = var.cloudflare_account_id
email = var.zt_admin_email
roles = [local.roles_by_name["Administrator"].id]
}
resource "cloudflare_account_member" "audit_viewer" {
account_id = var.cloudflare_account_id
email = var.audit_viewer_email
roles = [local.roles_by_name["Administrator Read Only"].id]
}
Code Pack: API Script
# List all account members and their roles
MEMBERS=$(cf_get "/accounts/${CF_ACCOUNT_ID}/members?per_page=50") || {
fail "1.4 Unable to retrieve account members"
increment_failed
summary
exit 0
}
MEMBER_COUNT=$(echo "${MEMBERS}" | jq '.result | length')
info "1.4 Found ${MEMBER_COUNT} account member(s)"
# Check for Super Administrator count
SUPER_ADMINS=$(echo "${MEMBERS}" | jq '[.result[] | select(.roles[]?.name == "Super Administrator")] | length')
if [ "${SUPER_ADMINS}" -gt 3 ]; then
warn "1.4 ${SUPER_ADMINS} Super Administrators found (recommend max 2-3)"
else
pass "1.4 ${SUPER_ADMINS} Super Administrator(s) found (within recommended limit)"
fi
# List all members with their roles
echo "${MEMBERS}" | jq -r '.result[] | " - \(.user.email): \([.roles[].name] | join(", "))"'
Code Pack: Sigma Detection Rule
detection:
selection:
ActionType: 'UpdateMember'
filter_role:
ActionMetadata|contains: 'Super Administrator'
condition: selection and filter_role
fields:
- ActorEmail
- ActionType
- ResourceID
- When
1.5 Retire the Global API Key and Enforce Scoped API Tokens
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 5.4, 6.8 |
| NIST 800-53 | AC-6(1), IA-5 |
Description
Eliminate use of the Global API Key — a single credential with full account-wide privileges that never expires — and replace it with scoped API tokens that carry explicit permissions, an expiry (TTL), and client IP restrictions. For automation, CI/CD, and Terraform, use Account-Owned API Tokens so credentials belong to the account rather than to an individual member. Cloudflare states directly that the Global API Key is not recommended and that customers should migrate to API tokens (Cloudflare API keys documentation).
Rationale
Why This Matters:
- The Global API Key grants full control of every zone and account setting the user can reach, so a single leaked key is equivalent to a full account takeover with no way to limit the damage short of rotating it
- The Global API Key has no scope, no expiry, and no IP restriction, which means a key pasted into a script, log, or support ticket stays valid indefinitely — Cloudflare rotated 104 API tokens after the 2025 Salesloft Drift incident precisely because credentials end up in support case text
- Scoped API tokens grant only the specific permissions a task needs (for example, DNS edit on one zone) and support a TTL and Client IP Address Filtering, so a stolen token is time-bound and usable only from expected networks
- Member-owned tokens die when the member is offboarded, silently breaking production automation; Account-Owned API Tokens (generally available since November 2024) are scoped to the account rather than to a person, so Terraform and CI/CD credentials survive admin turnover and are managed centrally by Super Administrators (Cloudflare blog: account-owned tokens)
Attack Prevented: Full-account takeover via leaked credentials, unlimited credential lifetime, lateral privilege escalation, orphaned automation credentials surviving offboarding
ClickOps Implementation
Step 1: Inventory Global API Key Usage
- Navigate to: Cloudflare Dashboard → My Profile → API Tokens
- Under API Keys, note whether the Global API Key has been viewed or distributed
- Search internal scripts, CI/CD secret stores, Terraform variable files, and runbooks for the header names
X-Auth-EmailandX-Auth-Key— these indicate Global API Key usage that must be migrated
Step 2: Create a Scoped User API Token
- Navigate to: My Profile → API Tokens → Create Token
- Start from a template or select Create Custom Token
- Under Permissions, grant only the specific product, scope, and level required (for example, Zone → DNS → Edit)
- Under Zone Resources / Account Resources, restrict the token to the exact zones or accounts it needs — never “All zones” unless genuinely required
- Under Client IP Address Filtering, add the egress IP or CIDR of the system that will use the token
- Under TTL, set an explicit start and expiry date rather than leaving the token permanent
- Click Continue to summary → Create Token and store the value in a secrets manager immediately (it is displayed only once)
Step 3: Create Account-Owned Tokens for Automation (L2)
- Navigate to: Manage Account → API Tokens (account scope, not profile scope)
- Click Create Token and configure permissions, resources, IP filtering, and TTL as above
- Use these tokens for Terraform, CI/CD pipelines, and any integration that must outlive an individual employee
- Restrict who can create and view account-owned tokens to Super Administrators
Step 4: Decommission the Global API Key
- Migrate every remaining consumer to a scoped token
- Navigate to: My Profile → API Tokens → API Keys → Global API Key → Change to roll the key, invalidating any copies that remain in circulation
- Repeat the roll for every account member who has ever retrieved their Global API Key
Time to Complete: ~60 minutes plus migration effort
Validation & Testing
- Confirm no running system authenticates with
X-Auth-Email/X-Auth-Keyheaders — all callers should send anAuthorization: Bearertoken instead - Verify each token’s restrictions by calling the token verification endpoint from an IP outside the allowlist; the request must fail
- Attempt an action outside the token’s granted permissions (for example, editing a zone the token does not cover) and confirm it is rejected
- Review Audit Logs for token creation events and confirm every token has a named owner and documented purpose
- Set a calendar reminder ahead of each token’s TTL expiry so rotation is planned rather than reactive
Compliance Mappings
| Framework | Control | Requirement |
|---|---|---|
| CIS Controls v8 | 5.4 | Restrict administrator privileges to dedicated administrator accounts |
| CIS Controls v8 | 6.8 | Define and maintain role-based access control |
| NIST 800-53 Rev 5 | AC-6(1) | Authorize access to security functions on a least-privilege basis |
| NIST 800-53 Rev 5 | IA-5 | Authenticator management, including lifetime and rotation |
| SOC 2 | CC6.1 | Logical access credentials are restricted and managed |
1.6 Enforce Two-Factor Authentication for Account Members
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.5 |
| NIST 800-53 | IA-2(1) |
Description
Turn on account-level 2FA Enforcement so that every Cloudflare account member must have two-factor authentication enabled before they can accept an invitation or continue using the account. This control protects the administrators of the platform itself and is distinct from the MFA you require of end users through Access policies (section 1.2) (Cloudflare two-factor authentication documentation).
Rationale
Why This Matters:
- Access policies enforce MFA for the people using protected applications, but they do nothing for the administrators who log into the Cloudflare dashboard — those accounts control DNS, WAF, Zero Trust policy, and tunnel configuration for the entire estate
- A single compromised administrator password without 2FA lets an attacker disable Gateway policies, publish a tunnel hostname without an Access application, or repoint DNS, defeating every other control in this guide at once
- Relying on individual members to enable 2FA voluntarily produces uneven coverage; account-level enforcement makes it a condition of membership, so a member who has not enrolled cannot accept an invite or keep using the account
- Enforcement applies continuously rather than only at invitation time, so members who disable 2FA later are prompted back into compliance instead of silently dropping below the baseline
Attack Prevented: Administrator credential theft, phishing of dashboard logins, password reuse leading to platform-wide configuration compromise, account takeover
ClickOps Implementation
Step 1: Enable 2FA on Your Own Account First
- Navigate to: My Profile → Authentication
- Under Two-Factor Authentication, click Manage
- Enrol an authenticator app or security key and complete verification
- Download and securely store the backup codes — enforcement will lock out an unenrolled Super Administrator
Step 2: Turn On Account-Level 2FA Enforcement
- Navigate to: Manage Account → Configurations → Authentication
- Locate Two-Factor Authentication Enforcement (available to Super Administrators)
- Enable enforcement for the account
- Members without 2FA are required to enable it before accepting an invitation or continuing to use the account
Step 3: Communicate and Remediate
- Notify members ahead of enabling enforcement so they can enrol without disruption
- Review Manage Account → Members for anyone in a pending or non-compliant state
- Prefer hardware security keys (WebAuthn) over one-time codes for Super Administrators, since keys resist real-time phishing relay
Step 4: Pair with SSO Where Available (L2)
- On Enterprise plans, configure SSO for dashboard login so administrator authentication inherits the IdP’s phishing-resistant factors and conditional access rules
- Keep 2FA enforcement enabled as a backstop for any account not covered by SSO
Time to Complete: ~30 minutes
Validation & Testing
- From Manage Account → Members, confirm every member shows a compliant 2FA status
- Invite a test member and confirm the invitation cannot be accepted until 2FA is configured
- Attempt a dashboard login with a valid password on a test account without 2FA and confirm the second factor is demanded
- Review Audit Logs for
2faand membership events to confirm enforcement was enabled and by whom - Re-check member compliance on a recurring schedule (at least quarterly) as part of access review
Compliance Mappings
| Framework | Control | Requirement |
|---|---|---|
| CIS Controls v8 | 6.5 | Require MFA for administrative access |
| NIST 800-53 Rev 5 | IA-2(1) | Multi-factor authentication to privileged accounts |
| SOC 2 | CC6.1 | Authentication controls restrict access to authorized users |
| ISO 27001:2022 | A.5.17 | Authentication information management |
2. Access Application Policies
2.1 Create Secure Application Policies
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.4 |
| NIST 800-53 | AC-3, AC-6 |
Description
Create Access policies that protect applications with identity-based, context-aware access controls.
Rationale
Why This Matters:
- Access policies define who can access each application
- Granular controls enable Zero Trust access
- Policies can require specific device posture
- Replaces VPN with identity-aware access
Attack Prevented: Unauthorized application access, lateral movement via broad VPN-style network access
ClickOps Implementation
Step 1: Add Application
- Navigate to: Access → Applications
- Click Add an application
- Select application type:
- Self-hosted: Applications behind Cloudflare Tunnel
- SaaS: Third-party SaaS applications
- Private network: Internal IP ranges
Step 2: Configure Application Settings
- Enter application details:
- Name: Descriptive application name
- Domain: Application URL
- Session duration: 24 hours (adjust as needed)
Step 3: Create Access Policy
- Click Add a policy
- Configure policy rules:
- Policy name: “Allow Engineering Team”
- Action: Allow
- Include rules:
- Emails ending in: @yourdomain.com
- IdP Groups: Engineering
- Require rules:
- Login methods: Your IdP
- Device posture: WARP running
Step 4: Harden Policy (L2)
- Add additional require rules:
- WARP: Require WARP client
- Device Posture: Require compliant device
- Location: Restrict to specific countries
- Add block rules for exceptions if needed
Time to Complete: ~30 minutes per application
Code Pack: Terraform
resource "cloudflare_zero_trust_access_group" "employees" {
account_id = var.cloudflare_account_id
name = "All Employees"
include = [{
email_domain = {
domain = var.corporate_domain
}
}]
}
resource "cloudflare_zero_trust_access_application" "internal_app" {
zone_id = var.cloudflare_zone_id
name = "Internal Application"
domain = var.app_domain
type = "self_hosted"
session_duration = "8h"
allowed_idps = [cloudflare_zero_trust_access_identity_provider.corporate_idp.id]
auto_redirect_to_identity = true
}
resource "cloudflare_zero_trust_access_policy" "allow_employees" {
account_id = var.cloudflare_account_id
name = "Allow authenticated employees"
decision = "allow"
include = [{
group = {
id = cloudflare_zero_trust_access_group.employees.id
}
}]
require = [{
auth_method = {
auth_method = "mfa"
}
}]
session_duration = "8h"
}
Code Pack: API Script
# List all Access applications and check policy configuration
APPS=$(cf_get "/accounts/${CF_ACCOUNT_ID}/access/apps") || {
fail "2.1 Unable to retrieve Access applications"
increment_failed
summary
exit 0
}
APP_COUNT=$(echo "${APPS}" | jq '.result | length')
info "2.1 Found ${APP_COUNT} Access application(s)"
UNPROTECTED=0
while IFS= read -r app_line; do
APP_ID=$(echo "${app_line}" | jq -r '.id')
APP_NAME=$(echo "${app_line}" | jq -r '.name')
APP_DOMAIN=$(echo "${app_line}" | jq -r '.domain // "N/A"')
POLICIES=$(cf_get "/accounts/${CF_ACCOUNT_ID}/access/apps/${APP_ID}/policies") || continue
POLICY_COUNT=$(echo "${POLICIES}" | jq '.result | length')
if [ "${POLICY_COUNT}" = "0" ]; then
warn "2.1 Application '${APP_NAME}' (${APP_DOMAIN}) has NO Access policies"
UNPROTECTED=$((UNPROTECTED + 1))
else
pass "2.1 Application '${APP_NAME}' has ${POLICY_COUNT} policy(s)"
fi
done < <(echo "${APPS}" | jq -c '.result[]')
Code Pack: Sigma Detection Rule
detection:
selection:
ActionType|contains:
- 'DeleteAccessPolicy'
- 'DeleteAccessApplication'
condition: selection
fields:
- ActorEmail
- ActionType
- ResourceID
- When
2.2 Require WARP for Application Access
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 4.1, 6.4 |
| NIST 800-53 | AC-2(11) |
Description
Configure Access policies to require WARP client for application access, enabling device posture checks and additional security controls.
Rationale
Why This Matters:
- Requiring WARP ensures every request to a protected application originates from a managed, enrolled device rather than an arbitrary browser
- WARP routes traffic through Gateway, so all access is subject to DNS, HTTP, and network inspection instead of bypassing security controls
- Device posture signals such as encryption, OS version, and security agents can only be evaluated when the WARP client is present and connected
- Blocking non-WARP access closes the gap where stolen credentials alone would otherwise be sufficient to reach sensitive apps
Attack Prevented: Unmanaged-device access, credential-only access, security-control bypass, data exfiltration
ClickOps Implementation
Step 1: Enable WARP Requirement in Policy
- Edit Access application policy
- Add Require rule:
- Selector: Require WARP
- Value: Enabled
- Save policy
Step 2: Configure WARP-Only Access
- For sensitive applications, block non-WARP access
- This ensures all traffic passes through Gateway for inspection
Code Pack: Terraform
resource "cloudflare_zero_trust_device_posture_rule" "warp_connected" {
account_id = var.cloudflare_account_id
name = "Require WARP Connected"
type = "warp"
description = "Ensure device is running WARP client"
match = [{
platform = "windows"
}, {
platform = "mac"
}, {
platform = "linux"
}]
}
resource "cloudflare_zero_trust_access_policy" "require_warp" {
account_id = var.cloudflare_account_id
name = "Require WARP for application access"
decision = "allow"
include = [{
email_domain = {
domain = var.corporate_domain
}
}]
require = [{
device_posture = {
integration_uid = cloudflare_zero_trust_device_posture_rule.warp_connected.id
}
}]
}
Code Pack: API Script
# Check for a WARP device posture rule
POSTURE_RULES=$(cf_get "/accounts/${CF_ACCOUNT_ID}/devices/posture") || {
fail "2.2 Unable to retrieve device posture rules"
increment_failed
summary
exit 0
}
WARP_RULES=$(echo "${POSTURE_RULES}" | jq '[.result[] | select(.type == "warp")] | length')
if [ "${WARP_RULES}" -gt 0 ]; then
pass "2.2 WARP device posture rule exists (${WARP_RULES} rule(s))"
echo "${POSTURE_RULES}" | jq -r '.result[] | select(.type == "warp") | " - \(.name): \(.type)"'
else
warn "2.2 No WARP device posture rule found -- create one to require WARP for app access"
fi
2.3 Configure Device Posture Checks
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 4.1 |
| NIST 800-53 | AC-2(11) |
Description
Define device posture checks to verify endpoint security status before granting application access.
Rationale
Why This Matters:
- Verified identity alone does not prove the device is safe — a legitimate user on a compromised or non-compliant laptop is still a threat
- Posture checks for disk encryption, firewall, screen lock, and OS version enforce a minimum security baseline before access is granted
- Service-provider checks confirm endpoint security tools such as EDR and anti-malware are actually running, not merely installed
- Blocking access on posture failure prevents malware-infected or out-of-date endpoints from reaching internal applications and data
Attack Prevented: Compromised-endpoint access, malware lateral movement, data exposure from unencrypted devices
ClickOps Implementation
Step 1: Create Device Posture Rules
- Navigate to: Settings → WARP Client → Device posture
- Click Add new
- Configure posture checks:
- OS version: Minimum required version
- Disk encryption: Required (FileVault/BitLocker)
- Firewall: Enabled
- Screen lock: Enabled
Step 2: Create Service Provider Check (Optional)
- Add checks for security tools:
- CrowdStrike running
- Carbon Black installed
- Custom certificate present
Step 3: Apply to Access Policy
- Edit application Access policy
- Add posture checks as Require rules
- Block access if checks fail
Code Pack: Terraform
resource "cloudflare_zero_trust_device_posture_rule" "disk_encryption" {
account_id = var.cloudflare_account_id
name = "Require Disk Encryption"
type = "disk_encryption"
description = "Ensure full-disk encryption is enabled (FileVault/BitLocker)"
schedule = "1h"
input = {
require_all = true
}
match = [{
platform = "windows"
}, {
platform = "mac"
}]
}
resource "cloudflare_zero_trust_device_posture_rule" "firewall_enabled" {
account_id = var.cloudflare_account_id
name = "Require Firewall Enabled"
type = "firewall"
description = "Ensure host firewall is enabled"
schedule = "1h"
match = [{
platform = "windows"
}, {
platform = "mac"
}]
}
resource "cloudflare_zero_trust_device_posture_rule" "os_version" {
account_id = var.cloudflare_account_id
name = "Minimum OS Version"
type = "os_version"
description = "Require minimum OS version"
schedule = "24h"
input = {
version = var.min_os_version
operator = ">="
}
match = [{
platform = "mac"
}]
}
Code Pack: API Script
# List all device posture rules
POSTURE_RULES=$(cf_get "/accounts/${CF_ACCOUNT_ID}/devices/posture") || {
fail "2.3 Unable to retrieve device posture rules"
increment_failed
summary
exit 0
}
RULE_COUNT=$(echo "${POSTURE_RULES}" | jq '.result | length')
info "2.3 Found ${RULE_COUNT} device posture rule(s)"
# Check for recommended posture checks
HAS_DISK=$(echo "${POSTURE_RULES}" | jq '[.result[] | select(.type == "disk_encryption")] | length')
HAS_FW=$(echo "${POSTURE_RULES}" | jq '[.result[] | select(.type == "firewall")] | length')
HAS_OS=$(echo "${POSTURE_RULES}" | jq '[.result[] | select(.type == "os_version")] | length')
[ "${HAS_DISK}" -gt 0 ] && pass "2.3 Disk encryption check configured" || warn "2.3 No disk encryption posture check found"
[ "${HAS_FW}" -gt 0 ] && pass "2.3 Firewall check configured" || warn "2.3 No firewall posture check found"
[ "${HAS_OS}" -gt 0 ] && pass "2.3 OS version check configured" || warn "2.3 No OS version posture check found"
echo "${POSTURE_RULES}" | jq -r '.result[] | " - \(.name) (\(.type))"'
2.4 Replace Long-Lived SSH Keys with Access for Infrastructure
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 6.4, 8.5 |
| NIST 800-53 | AC-17, IA-5(2), AU-14 |
Description
Use Cloudflare Access for Infrastructure to broker SSH sessions with short-lived certificates issued at login rather than long-lived private keys distributed to engineers. Targets are registered in Zero Trust, policies bind an identity to a specific target and Unix username, and session commands can be recorded and stored encrypted (Cloudflare SSH with Access for Infrastructure documentation).
Rationale
Why This Matters:
- Long-lived SSH private keys sit on laptops, in build agents, and in backup archives indefinitely; anyone who obtains a copy has persistent access that no identity provider decision can revoke
- Short-lived certificates are minted only after a successful Access login, so revoking a user in the IdP or failing a device posture check immediately ends their ability to obtain new SSH sessions
- Per-target and per-Unix-username policies stop the common pattern where a single shared key grants root-equivalent access to a whole fleet, limiting what one compromised identity can reach
- Encrypted SSH command logging produces a per-session record of what was actually executed, which is essential for insider-threat investigation and for demonstrating privileged-session accountability to auditors
- Because the target is reached through a Cloudflare Tunnel, the SSH port never needs to be exposed to the internet, removing it from the reach of credential-stuffing and scanning bots
Attack Prevented: Stolen or copied SSH private keys, persistent access after offboarding, lateral movement via shared keys, unaudited privileged sessions, internet-exposed SSH brute forcing
Prerequisites
- A Cloudflare Tunnel connecting the target network (see section 5.1)
- WARP deployed and enrolled on the client devices that will connect
- An identity provider configured (see section 1.1)
ClickOps Implementation
Step 1: Route the Target Network Through a Tunnel
- Navigate to: Zero Trust → Networks → Tunnels
- Select or create the tunnel serving the environment that hosts the SSH servers
- Under Private Network, add the CIDR range containing the target hosts
Step 2: Register Infrastructure Targets
- Navigate to: Zero Trust → Networks → Targets
- Click Add a target
- Enter the target hostname, IP address, and the virtual network it belongs to
- Repeat for each server that should be reachable over SSH
Step 3: Create an Infrastructure Application
- Navigate to: Zero Trust → Access controls → Applications
- Click Add an application and select Infrastructure
- Select the targets to include and set the protocol to SSH with port 22
- Add a policy: set Action to Allow, include your IdP group (for example, Platform Engineering), and add Require rules for login method and device posture
- Under the policy’s connection context, specify the exact Unix usernames the group may assume — avoid granting
rootwhere a named account will do
Step 4: Configure the Server to Trust the Cloudflare SSH CA
- In the application configuration, download the Cloudflare SSH CA public key for your account
- On each target host, install the CA public key and point
TrustedUserCAKeysat it in the SSH daemon configuration - Restart the SSH daemon and confirm certificate-based authentication succeeds
- Once verified, disable password authentication and remove distributed
authorized_keysentries that are no longer needed
Step 5: Enable SSH Command Logging (L3)
- In the Infrastructure application settings, enable SSH command logging
- Provide the public key used to encrypt session logs and store the corresponding private key in your secrets manager
- Configure a Logpush job to deliver the encrypted session logs to your SIEM or object storage
Time to Complete: ~90 minutes for the first target, ~15 minutes per additional target
Validation & Testing
- Connect to a target as an authorized user and confirm the session succeeds without any local private key present
- Remove the test user from the IdP group and confirm a new connection attempt is denied while no long-lived key remains that would still work
- Attempt to connect as a Unix username not listed in the policy and confirm the session is refused
- Attempt to reach port 22 on the target directly from the public internet and confirm there is no listener
- Retrieve an encrypted session log, decrypt it with the stored private key, and confirm the executed commands are recorded
Compliance Mappings
| Framework | Control | Requirement |
|---|---|---|
| CIS Controls v8 | 6.4 | Require MFA for remote network access |
| CIS Controls v8 | 8.5 | Collect detailed audit logs for privileged activity |
| NIST 800-53 Rev 5 | AC-17 | Remote access authorization, monitoring, and control |
| NIST 800-53 Rev 5 | IA-5(2) | Public key-based authentication |
| NIST 800-53 Rev 5 | AU-14 | Session audit and recording |
| SOC 2 | CC6.6 | Access to infrastructure is restricted to authorized personnel |
2.5 Gate Access Policies on User Risk Score
Profile Level: L3 (Run)
| Framework | Control |
|---|---|
| CIS Controls | 13.1 |
| NIST 800-53 | AC-2(12) |
Description
Enable Cloudflare’s behavioural risk scoring and use the resulting Low, Medium, or High score as a condition in Access policies, so that users exhibiting suspicious behaviour are blocked or forced to reauthenticate rather than continuing with the access their group membership would normally grant (Cloudflare user risk score documentation).
Rationale
Why This Matters:
- Static policies evaluate identity and device at the moment of login, so an account that is compromised after enrolment keeps its access until someone notices and intervenes manually
- Risk behaviours such as impossible travel, repeated DLP policy violations, and contact with known malware infrastructure are strong signals that an account or endpoint is under adversary control
- Binding a High risk score to a Block or reauthentication action turns detection into enforcement automatically, closing the gap between an alert firing and an analyst responding
- Every risk behaviour is disabled by default, so an organisation that assumes risk scoring is on out of the box is operating with no behavioural signal at all — the behaviours must be explicitly enabled to produce scores
- Scores are per-user and visible in the dashboard, giving investigators a prioritised queue rather than an undifferentiated stream of Gateway and Access logs
Attack Prevented: Session hijacking and account takeover after initial login, insider data exfiltration, continued access by a compromised endpoint, credential sharing across geographies
Prerequisites
- Cloudflare Zero Trust Enterprise plan
- WARP deployed with Gateway logging enabled so behavioural signals are collected
- Identity provider integration configured (see section 1.1)
ClickOps Implementation
Step 1: Enable Risk Behaviours
- Navigate to: Zero Trust → Teams & Resources → Users → Risk score
- Review the available behaviours — all are disabled by default
- Enable the behaviours relevant to your environment, such as impossible travel, high number of DLP policy violations, and contact with known malware or command-and-control destinations
- Set the risk level each behaviour contributes (Low, Medium, or High) to match your tolerance
Step 2: Reference Risk Score in Access Policies
- Navigate to: Zero Trust → Access controls → Applications and edit a sensitive application
- Add a policy with Action set to Block and an include rule using the User Risk Score selector set to High
- Order the block policy above the allow policies so it is evaluated first
- For lower-sensitivity applications, use a Medium score to trigger reauthentication rather than an outright block
Step 3: Extend Risk Gating to Gateway (L3)
- Navigate to: Gateway → Firewall Policies and add policies matching on user risk score
- Restrict high-risk users from reaching sensitive SaaS destinations or from uploading data
Step 4: Define the Response Runbook
- Document who reviews the risk score dashboard and how often
- Define the criteria for clearing a user’s risk score after investigation, and record who is authorised to clear it
- Feed risk score changes into your SIEM through Logpush so they correlate with other alerts
Time to Complete: ~60 minutes
Validation & Testing
- Confirm the behaviours you intended to enable show as enabled on the risk score page — verify rather than assume, since the default is off
- Trigger a test behaviour in a controlled way (for example, a deliberate DLP policy violation by a test account) and confirm the user’s score changes
- Attempt to reach a protected application as the elevated-risk test user and confirm the block or reauthentication policy fires
- Confirm the block appears in Access logs with the risk score as the reason
- Clear the test user’s risk score and confirm normal access is restored
Compliance Mappings
| Framework | Control | Requirement |
|---|---|---|
| CIS Controls v8 | 13.1 | Centralize security event alerting and response |
| NIST 800-53 Rev 5 | AC-2(12) | Account monitoring for atypical usage |
| NIST 800-53 Rev 5 | SI-4 | System monitoring and automated response |
| SOC 2 | CC7.2 | Anomalies are detected and evaluated |
3. Gateway Security Policies
3.1 Configure DNS Filtering
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 9.2 |
| NIST 800-53 | SC-7, SI-3 |
Description
Configure Gateway DNS policies to block access to malicious and policy-violating domains.
Rationale
Why This Matters:
- DNS filtering blocks threats at the resolution layer
- Prevents access to malware, phishing, and C2 domains
- Works for all traffic, not just HTTP(S)
- Cloudflare’s threat intelligence provides real-time protection
Attack Prevented: Malware delivery, phishing, and command-and-control callbacks at the DNS resolution layer
ClickOps Implementation
Step 1: Create DNS Policy
- Navigate to: Gateway → Firewall Policies → DNS
- Click Add a policy
- Configure blocking rules:
Step 2: Block Security Threats
- Create rule: “Block Security Threats”
- Configure:
- Selector: Security Categories
- Operator: in
- Value: Malware, Phishing, Spyware, Botnet, Cryptomining, Command and Control
- Action: Block
- Save
Step 3: Block Content Categories (Policy)
- Create additional rules for policy enforcement:
- Adult Content
- Gambling
- Illegal Activities
- Configure action: Block or Override (with warning)
Time to Complete: ~30 minutes
Code Pack: Terraform
resource "cloudflare_zero_trust_gateway_policy" "block_security_threats_dns" {
account_id = var.cloudflare_account_id
name = "Block Security Threats (DNS)"
action = "block"
filters = ["dns"]
traffic = "any(dns.security_category[*] in {80 83 176 178})"
enabled = true
precedence = 10
rule_settings = {
block_page_enabled = true
block_reason = "Blocked: malware, phishing, spyware, or C2 domain"
}
}
resource "cloudflare_zero_trust_gateway_policy" "block_content_categories_dns" {
account_id = var.cloudflare_account_id
name = "Block Restricted Content Categories (DNS)"
action = "block"
filters = ["dns"]
traffic = "any(dns.content_category[*] in {133 134 135 136})"
enabled = true
precedence = 20
rule_settings = {
block_page_enabled = true
block_reason = "This content category is blocked by policy"
}
}
Code Pack: API Script
# Create Gateway DNS policy to block security threats
EXISTING=$(cf_get "/accounts/${CF_ACCOUNT_ID}/gateway/rules") || {
fail "3.1 Unable to retrieve Gateway rules"
increment_failed
summary
exit 0
}
DNS_BLOCK_RULES=$(echo "${EXISTING}" | jq '[.result[] | select(.filters == ["dns"] and .action == "block")] | length')
if [ "${DNS_BLOCK_RULES}" -gt 0 ]; then
pass "3.1 Found ${DNS_BLOCK_RULES} DNS blocking rule(s) already configured"
echo "${EXISTING}" | jq -r '.result[] | select(.filters == ["dns"] and .action == "block") | " - \(.name)"'
increment_applied
summary
exit 0
fi
info "3.1 Creating DNS security threat blocking rule..."
RESPONSE=$(cf_post "/accounts/${CF_ACCOUNT_ID}/gateway/rules" '{
"name": "HTH: Block Security Threats (DNS)",
"action": "block",
"filters": ["dns"],
"traffic": "any(dns.security_category[*] in {80 83 176 178})",
"enabled": true,
"precedence": 10,
"rule_settings": {
"block_page_enabled": true,
"block_reason": "Blocked: malware, phishing, spyware, or C2 domain"
}
}') || {
fail "3.1 Failed to create DNS blocking rule"
increment_failed
summary
exit 0
}
Code Pack: Sigma Detection Rule
detection:
selection:
ActionType|contains:
- 'DeleteGatewayRule'
- 'UpdateGatewayRule'
filter_dns:
ActionMetadata|contains: 'dns'
condition: selection and filter_dns
fields:
- ActorEmail
- ActionType
- ResourceID
- When
3.2 Configure HTTP Filtering
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 9.2, 13.3 |
| NIST 800-53 | SC-7, SI-4 |
Description
Configure Gateway HTTP policies for deeper inspection and control of web traffic.
Rationale
Why This Matters:
- DNS filtering alone cannot see inside HTTP(S) sessions — Layer 7 inspection is needed to block malicious downloads and specific URLs
- HTTP policies stop malware and botnet content even when delivered from otherwise-reputable or newly-categorized domains
- Inline file inspection and antivirus scanning intercept malicious payloads before they reach the endpoint
- Web-layer control reduces the chance that a single drive-by download or malicious file leads to endpoint compromise
Attack Prevented: Malware downloads, drive-by compromise, botnet communication, malicious file delivery
ClickOps Implementation
Step 1: Create HTTP Policy
- Navigate to: Gateway → Firewall Policies → HTTP
- Click Add a policy
Step 2: Block Malicious Content
- Create rule: “Block Malware Downloads”
- Configure:
- Selector: Content Categories
- Operator: in
- Value: Malware, Botnet
- Action: Block
Step 3: Inspect File Downloads (L2)
- Create rule for file inspection
- Configure AV scanning for downloads
- Block or quarantine detected threats
Code Pack: Terraform
resource "cloudflare_zero_trust_gateway_policy" "block_malware_http" {
account_id = var.cloudflare_account_id
name = "Block Malware Downloads (HTTP)"
action = "block"
filters = ["http"]
traffic = "any(http.request.uri.content_category[*] in {80 83})"
enabled = true
precedence = 10
rule_settings = {
block_page_enabled = true
block_reason = "Blocked: malware risk detected in download"
}
}
resource "cloudflare_zero_trust_gateway_policy" "av_scan_downloads" {
account_id = var.cloudflare_account_id
name = "Scan file downloads for threats"
action = "block"
filters = ["http"]
traffic = "any(http.request.uri.content_category[*] in {80}) and http.request.method == \"GET\""
enabled = true
precedence = 15
rule_settings = {
block_page_enabled = true
block_reason = "File blocked: threat detected during scan"
}
}
Code Pack: API Script
# Create Gateway HTTP policy to block malware downloads
EXISTING=$(cf_get "/accounts/${CF_ACCOUNT_ID}/gateway/rules") || {
fail "3.2 Unable to retrieve Gateway rules"
increment_failed
summary
exit 0
}
HTTP_BLOCK_RULES=$(echo "${EXISTING}" | jq '[.result[] | select(.filters == ["http"] and .action == "block")] | length')
if [ "${HTTP_BLOCK_RULES}" -gt 0 ]; then
pass "3.2 Found ${HTTP_BLOCK_RULES} HTTP blocking rule(s) already configured"
increment_applied
summary
exit 0
fi
info "3.2 Creating HTTP malware download blocking rule..."
RESPONSE=$(cf_post "/accounts/${CF_ACCOUNT_ID}/gateway/rules" '{
"name": "HTH: Block Malware Downloads (HTTP)",
"action": "block",
"filters": ["http"],
"traffic": "any(http.request.uri.content_category[*] in {80 83})",
"enabled": true,
"precedence": 10,
"rule_settings": {
"block_page_enabled": true,
"block_reason": "Blocked: malware risk detected in download"
}
}') || {
fail "3.2 Failed to create HTTP blocking rule"
increment_failed
summary
exit 0
}
3.3 Configure Network Policies
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 4.4, 13.4 |
| NIST 800-53 | SC-7, AC-4 |
Description
Configure Gateway network policies to control non-HTTP traffic based on IP, port, and protocol.
Rationale
Why This Matters:
- Threats and data exfiltration frequently use non-HTTP channels that web and DNS filtering never inspect
- Blocking risky ports, tunneling, and P2P protocols removes covert paths attackers use for command-and-control and lateral movement
- Identity-based controls on the private network range (100.96.0.0/12) prevent any enrolled user from freely reaching internal systems
- Logging private network access creates the audit trail needed to detect and investigate unauthorized internal connections
Attack Prevented: Command-and-control over non-HTTP ports, data exfiltration, lateral movement, unauthorized internal access
ClickOps Implementation
Step 1: Create Network Policy
- Navigate to: Gateway → Firewall Policies → Network
- Click Add a policy
Step 2: Block Risky Protocols
- Create rules blocking:
- Known malicious ports
- Tunneling protocols (if not allowed)
- P2P protocols
- Configure action: Block
Step 3: Control Private Network Access
- If using WARP-to-WARP (private network):
- Create policies for 100.96.0.0/12 range
- Restrict access by user identity
- Log all private network access
Code Pack: Terraform
resource "cloudflare_zero_trust_gateway_policy" "block_risky_protocols" {
account_id = var.cloudflare_account_id
name = "Block SSH to external hosts"
action = "block"
filters = ["l4"]
traffic = "net.dst.port == 22 and net.dst.ip !in {10.0.0.0/8 172.16.0.0/12 192.168.0.0/16}"
enabled = true
precedence = 10
}
resource "cloudflare_zero_trust_gateway_policy" "audit_rdp" {
account_id = var.cloudflare_account_id
name = "Audit RDP connections"
action = "allow"
filters = ["l4"]
traffic = "net.dst.port == 3389"
enabled = true
precedence = 20
}
Code Pack: API Script
# Create Gateway network policy to block risky protocols
EXISTING=$(cf_get "/accounts/${CF_ACCOUNT_ID}/gateway/rules") || {
fail "3.3 Unable to retrieve Gateway rules"
increment_failed
summary
exit 0
}
L4_RULES=$(echo "${EXISTING}" | jq '[.result[] | select(.filters == ["l4"])] | length')
if [ "${L4_RULES}" -gt 0 ]; then
pass "3.3 Found ${L4_RULES} network (L4) rule(s) already configured"
increment_applied
summary
exit 0
fi
info "3.3 Creating network policy to block external SSH..."
RESPONSE=$(cf_post "/accounts/${CF_ACCOUNT_ID}/gateway/rules" '{
"name": "HTH: Block External SSH",
"action": "block",
"filters": ["l4"],
"traffic": "net.dst.port == 22 and net.dst.ip !in {10.0.0.0/8 172.16.0.0/12 192.168.0.0/16}",
"enabled": true,
"precedence": 10
}') || {
fail "3.3 Failed to create network blocking rule"
increment_failed
summary
exit 0
}
3.4 Enable Browser Isolation (L3)
Profile Level: L3 (Run)
| Framework | Control |
|---|---|
| CIS Controls | 10.5 |
| NIST 800-53 | SI-3 |
Description
Enable Cloudflare Browser Isolation to execute web sessions in a secure cloud environment, preventing malware execution on endpoints.
Rationale
Why This Matters:
- Running web sessions in a remote cloud browser means active web code never executes on the endpoint, neutralizing browser-borne malware and zero-days
- Isolating uncategorized and newly-registered domains contains the highest-risk browsing where threat intelligence has not yet caught up
- Disabling copy/paste, printing, and uploads/downloads on sensitive sites prevents data from leaving controlled sessions
- Isolation protects against exploit kits and malicious scripts even when users click links that slip past other filters
Attack Prevented: Browser-based malware, drive-by downloads, zero-day exploits, web-based data exfiltration
Prerequisites
- Browser Isolation add-on license
ClickOps Implementation
Step 1: Create Isolation Policy
- Navigate to: Gateway → Firewall Policies → HTTP
- Create rule with Action: Isolate
- Configure targets:
- Uncategorized domains
- Newly registered domains
- High-risk categories
Step 2: Configure Isolation Settings
- In Settings → Browser Isolation
- Configure:
- Disable copy/paste: For sensitive sites
- Disable printing: For sensitive sites
- Disable uploads/downloads: Based on policy
Code Pack: Terraform
resource "cloudflare_zero_trust_gateway_policy" "isolate_risky_sites" {
account_id = var.cloudflare_account_id
name = "Isolate risky and uncategorized websites"
action = "isolate"
filters = ["http"]
traffic = "any(http.request.uri.content_category[*] in {68 155})"
enabled = true
precedence = 5
rule_settings = {
biso_admin_controls = {
copy = "remote_only"
paste = "block"
download = "block"
upload = "block"
printing = "block"
keyboard = "allow"
}
}
}
Code Pack: API Script
# Create Gateway HTTP policy with isolate action for risky sites
EXISTING=$(cf_get "/accounts/${CF_ACCOUNT_ID}/gateway/rules") || {
fail "3.4 Unable to retrieve Gateway rules"
increment_failed
summary
exit 0
}
ISOLATE_RULES=$(echo "${EXISTING}" | jq '[.result[] | select(.action == "isolate")] | length')
if [ "${ISOLATE_RULES}" -gt 0 ]; then
pass "3.4 Found ${ISOLATE_RULES} browser isolation rule(s) already configured"
increment_applied
summary
exit 0
fi
info "3.4 Creating browser isolation policy for risky sites..."
RESPONSE=$(cf_post "/accounts/${CF_ACCOUNT_ID}/gateway/rules" '{
"name": "HTH: Isolate Risky Websites",
"action": "isolate",
"filters": ["http"],
"traffic": "any(http.request.uri.content_category[*] in {68 155})",
"enabled": true,
"precedence": 5,
"rule_settings": {
"biso_admin_controls": {
"dcp": true,
"dd": true,
"du": true,
"dp": true,
"dk": false
}
}
}') || {
fail "3.4 Failed to create browser isolation rule"
increment_failed
summary
exit 0
}
3.5 Configure Gateway Data Loss Prevention Profiles
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 3.13 |
| NIST 800-53 | AC-4, SC-7(10) |
Description
Use Gateway DLP profiles to inspect HTTP and SaaS traffic for sensitive data and block or log transfers that match. Two predefined profiles — Financial Information, and Social Security, Insurance, and Tax Identification Numbers — are available even on Free and Pay-as-you-go plans; custom profiles and additional detection entries require the Enterprise DLP add-on (Cloudflare data loss prevention documentation).
Rationale
Why This Matters:
- Gateway HTTP filtering blocks what is coming in, but without DLP nothing inspects what is leaving — payment card numbers, national identifiers, and source code can be pasted into a personal cloud drive or an AI chat interface with no record and no control
- Two predefined profiles are usable on Free and Pay-as-you-go plans, so the common assumption that DLP requires an Enterprise purchase leaves basic coverage unused at no additional cost
- DLP policies match on the payload rather than the destination category, catching exfiltration to a newly registered or uncategorised domain that reputation-based filtering would allow
- DLP detections feed the user risk score (see section 2.5), so a user repeatedly triggering DLP policies can be automatically escalated and blocked rather than merely logged
- Running DLP in log-only mode first produces the evidence needed to tune profiles before enforcement, avoiding the false-positive backlash that causes teams to disable DLP entirely
Attack Prevented: Data exfiltration by insiders or compromised accounts, unintentional disclosure of regulated data to unsanctioned SaaS, sensitive data pasted into third-party AI or file-sharing services
Prerequisites
- Gateway HTTP filtering enabled with TLS inspection configured (see section 3.2)
- WARP deployed with the Cloudflare root certificate installed on managed devices
ClickOps Implementation
Step 1: Enable TLS Inspection
- Navigate to: Zero Trust → Settings → Network
- Enable TLS decryption — DLP cannot inspect payloads inside encrypted sessions without it
- Confirm the Cloudflare root certificate is deployed to managed devices, and document any inspection bypasses required for banking or healthcare sites
Step 2: Review the Predefined Profiles
- Navigate to: Zero Trust → DLP → DLP profiles
- Open Financial Information and review its detection entries (payment card numbers and similar)
- Open Social Security, Insurance, and Tax Identification Numbers and review its entries
- Set the confidence threshold and minimum match count on each entry to reduce false positives
Step 3: Create a Log-Only HTTP Policy
- Navigate to: Gateway → Firewall Policies → HTTP
- Add a policy with the DLP Profile selector set to the profiles enabled above
- Set Action to Allow with logging so matches are recorded without blocking
- Run for one to two weeks and review matches in Gateway HTTP logs
Step 4: Move to Enforcement
- After tuning, change the action to Block for the highest-confidence profiles
- Scope enforcement by destination where appropriate — for example, block uploads of matched data to personal file-sharing and unsanctioned AI services while allowing sanctioned applications
- Configure a custom block page explaining why the upload was stopped and how to request an exception
Step 5: Add Custom Profiles (L3, Enterprise DLP add-on)
- Navigate to: DLP → DLP profiles → Create profile
- Define custom detection entries for organisation-specific identifiers such as customer account number formats or internal project codenames
- Apply the same log-then-enforce progression before blocking
Time to Complete: ~60 minutes to configure, plus one to two weeks of tuning
Validation & Testing
- From a WARP-enrolled test device, upload a file containing synthetic test data matching a predefined profile (use documented test values, never real customer data) and confirm the match appears in Gateway HTTP logs
- After enforcement is enabled, repeat the upload and confirm it is blocked and the block page is displayed
- Confirm that traffic on documented TLS inspection bypass lists is not inspected, and that this exposure is accepted and recorded
- Review one week of DLP matches and calculate the false-positive rate before widening enforcement
- Confirm DLP match events reach your SIEM through Logpush
Compliance Mappings
| Framework | Control | Requirement |
|---|---|---|
| CIS Controls v8 | 3.13 | Deploy a data loss prevention solution |
| NIST 800-53 Rev 5 | AC-4 | Information flow enforcement |
| NIST 800-53 Rev 5 | SC-7(10) | Prevent exfiltration of information |
| SOC 2 | CC6.7 | Transmission of sensitive data is restricted and monitored |
| PCI DSS v4.0 | 3.2 | Limit storage and transmission of cardholder data |
4. WARP Client Hardening
4.1 Configure WARP Client Settings
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 4.1 |
| NIST 800-53 | CM-7, SC-7 |
Description
Configure WARP client settings to ensure consistent security posture across all enrolled devices.
Rationale
Why This Matters:
- Consistent global settings ensure every enrolled device enforces the same Zero Trust protections rather than relying on per-user configuration
- Auto-connect and captive-portal detection keep WARP active across reboots and untrusted WiFi, closing windows where traffic would bypass inspection
- Locking the WARP switch prevents users from disabling protection to evade filtering or reach blocked content
- A defined default service mode (Gateway with WARP) guarantees traffic is routed through inspection by default, not left to user choice
Attack Prevented: Protection bypass, unfiltered traffic on untrusted networks, inconsistent endpoint posture
ClickOps Implementation
Step 1: Access WARP Settings
- Navigate to: Settings → WARP Client
- Click Manage under Global settings
Step 2: Configure Global Settings
- Auto connect: Enable (reconnect after disconnection)
- Captive portal detection: Enable (for WiFi networks)
- Mode switch: Configure default mode (Gateway with WARP)
Step 3: Configure Lock Settings (L2)
- Lock WARP switch: Enable (prevent user disable)
- Allow admin override: Enable with codes (for troubleshooting)
- Disable for WiFi: Configure trusted network exception
Code Pack: Terraform
resource "cloudflare_zero_trust_device_default_profile" "default" {
account_id = var.cloudflare_account_id
auto_connect = 0
captive_portal = 180
allow_mode_switch = false
allow_updates = true
tunnel_protocol = "wireguard"
service_mode_v2 = {
mode = "warp"
}
}
Code Pack: API Script
# Configure default WARP device settings
CURRENT=$(cf_get "/accounts/${CF_ACCOUNT_ID}/devices/policy") || {
fail "4.1 Unable to retrieve device policy"
increment_failed
summary
exit 0
}
info "4.1 Current device policy settings:"
echo "${CURRENT}" | jq '.result | {
auto_connect: .auto_connect,
captive_portal: .captive_portal,
allow_mode_switch: .allow_mode_switch,
switch_locked: .switch_locked,
tunnel_protocol: .tunnel_protocol
}'
# Apply hardened settings
RESPONSE=$(cf_patch "/accounts/${CF_ACCOUNT_ID}/devices/policy" '{
"auto_connect": 0,
"captive_portal": 180,
"allow_mode_switch": false,
"tunnel_protocol": "wireguard"
}') || {
fail "4.1 Failed to update device policy"
increment_failed
summary
exit 0
}
4.2 Lock WARP Client
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 4.1 |
| NIST 800-53 | CM-7 |
Description
Lock WARP client to prevent users from disabling Zero Trust protection.
Rationale
Why This Matters:
- If users can freely disable WARP, all Gateway filtering and Access posture checks can be bypassed at will, defeating the Zero Trust model
- Locking the switch keeps every device continuously inspected, including when malware or a user actively tries to evade controls
- Admin override codes preserve a controlled, time-limited path for legitimate troubleshooting without leaving the switch open to everyone
- Enforcing Gateway with WARP service mode ensures all traffic remains filtered rather than silently falling back to an unprotected path
Attack Prevented: Security-control evasion, unfiltered malicious traffic, posture-check bypass
ClickOps Implementation
Step 1: Enable Lock Settings
- Navigate to: Settings → WARP Client → Device settings
- Create or edit device profile
- Enable Lock WARP switch
Step 2: Configure Override Codes (Optional)
- Enable Allow admin override codes
- Admins can generate temporary disable codes
- Codes can be time-limited
Step 3: Configure Service Mode
- Set Service mode: Gateway with WARP
- This ensures all traffic is filtered
- Alternative modes available for specific needs
Code Pack: Terraform
resource "cloudflare_zero_trust_device_default_profile" "locked" {
account_id = var.cloudflare_account_id
switch_locked = true
allowed_to_leave = false
allow_mode_switch = false
auto_connect = 0
}
Code Pack: API Script
# Lock WARP client to prevent users from disabling
info "4.2 Locking WARP client..."
RESPONSE=$(cf_patch "/accounts/${CF_ACCOUNT_ID}/devices/policy" '{
"switch_locked": true,
"allowed_to_leave": false,
"allow_mode_switch": false
}') || {
fail "4.2 Failed to lock WARP client"
increment_failed
summary
exit 0
}
Code Pack: Sigma Detection Rule
detection:
selection:
ActionType: 'UpdateDevicePolicy'
filter_unlock:
ActionMetadata|contains:
- 'switch_locked'
- 'allowed_to_leave'
condition: selection and filter_unlock
fields:
- ActorEmail
- ActionType
- ResourceID
- When
4.3 Configure Split Tunnel Settings
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 13.5 |
| NIST 800-53 | SC-7 |
Description
Configure split tunnel settings to control which traffic passes through WARP and which bypasses.
Rationale
Why This Matters:
- By default, all traffic goes through WARP (full tunnel)
- Split tunnel can improve performance for specific apps
- Excessive split tunnel reduces security visibility
- Document all exceptions with business justification
Attack Prevented: Data exfiltration and threats hidden in traffic bypassing WARP inspection via excessive split-tunnel exceptions
ClickOps Implementation
Step 1: Access Split Tunnel Settings
- Navigate to: Settings → WARP Client → Device settings
- Select device profile
- Click Configure under Split Tunnels
Step 2: Configure Minimum Exceptions
- Mode: Exclude IPs and domains (default is include all)
- Add only necessary exceptions:
- Video conferencing (Zoom, Teams IPs)
- Local network access (RFC1918)
- Document each exception
Step 3: Prefer Include Mode (L3)
- For maximum security, use Include mode
- Only specified traffic goes through WARP
- Everything else uses local network
Code Pack: Terraform
resource "cloudflare_zero_trust_device_default_profile" "split_tunnel" {
account_id = var.cloudflare_account_id
switch_locked = true
exclude = [{
address = "10.0.0.0/8"
description = "Internal RFC1918"
}, {
address = "172.16.0.0/12"
description = "Internal RFC1918"
}, {
address = "192.168.0.0/16"
description = "Internal RFC1918"
}]
}
Code Pack: API Script
# Audit split tunnel exclude list
EXCLUDE_LIST=$(cf_get "/accounts/${CF_ACCOUNT_ID}/devices/policy/exclude") || {
fail "4.3 Unable to retrieve split tunnel exclude list"
increment_failed
summary
exit 0
}
EXCLUDE_COUNT=$(echo "${EXCLUDE_LIST}" | jq '.result | length')
info "4.3 Found ${EXCLUDE_COUNT} split tunnel exclusion(s)"
if [ "${EXCLUDE_COUNT}" -gt 20 ]; then
warn "4.3 ${EXCLUDE_COUNT} exclusions is excessive -- review and minimize exceptions"
else
pass "4.3 Split tunnel exclusion count (${EXCLUDE_COUNT}) is within reasonable range"
fi
echo "${EXCLUDE_LIST}" | jq -r '.result[] | " - \(.address // .host // "unknown"): \(.description // "no description")"'
5. Tunnel Security
5.1 Secure Cloudflare Tunnel Configuration
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 12.1 |
| NIST 800-53 | SC-7, SC-8 |
Description
Configure Cloudflare Tunnel (formerly Argo Tunnel) securely to expose internal applications without opening inbound ports.
Rationale
Why This Matters:
- Tunnels eliminate inbound firewall rules
- Misconfigured tunnels can expose internal services
- Access policies must protect tunnel endpoints
- Tunnel credentials must be secured
Attack Prevented: Internal service exposure via misconfigured tunnels, tunnel credential theft, unauthenticated access to tunnel endpoints
ClickOps Implementation
Step 1: Create Tunnel
- Navigate to: Access → Tunnels
- Click Create a tunnel
- Name the tunnel descriptively
- Install cloudflared on origin server
Step 2: Configure Public Hostname
- Add public hostname routing
- Configure:
- Subdomain: app.yourdomain.com
- Service: http://localhost:8080
- Always create Access policy before exposing
Step 3: Secure Tunnel Credentials
- Tunnel token should be treated as secret
- Store securely (vault, secrets manager)
- Rotate if compromised
Code Pack: Terraform
resource "random_id" "tunnel_secret" {
byte_length = 35
}
resource "cloudflare_zero_trust_tunnel_cloudflared" "app_tunnel" {
account_id = var.cloudflare_account_id
name = "app-tunnel"
config_src = "cloudflare"
tunnel_secret = random_id.tunnel_secret.b64_std
}
resource "cloudflare_zero_trust_tunnel_cloudflared_config" "app_tunnel_config" {
account_id = var.cloudflare_account_id
tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.app_tunnel.id
config = {
ingress = [{
hostname = var.app_domain
service = var.app_origin_url
origin_request = {
connect_timeout = 10
no_tls_verify = false
}
}, {
service = "http_status:404"
}]
}
}
resource "cloudflare_dns_record" "tunnel_cname" {
zone_id = var.cloudflare_zone_id
name = var.app_subdomain
type = "CNAME"
content = "${cloudflare_zero_trust_tunnel_cloudflared.app_tunnel.id}.cfargotunnel.com"
proxied = true
}
Code Pack: API Script
# List all tunnels and check configuration
TUNNELS=$(cf_get "/accounts/${CF_ACCOUNT_ID}/cfd_tunnel?is_deleted=false") || {
fail "5.1 Unable to retrieve tunnel list"
increment_failed
summary
exit 0
}
TUNNEL_COUNT=$(echo "${TUNNELS}" | jq '.result | length')
info "5.1 Found ${TUNNEL_COUNT} active tunnel(s)"
while IFS= read -r tunnel; do
TUNNEL_ID=$(echo "${tunnel}" | jq -r '.id')
TUNNEL_NAME=$(echo "${tunnel}" | jq -r '.name')
TUNNEL_STATUS=$(echo "${tunnel}" | jq -r '.status')
REMOTE_CONFIG=$(echo "${tunnel}" | jq -r '.remote_config // false')
echo -e " Tunnel: ${TUNNEL_NAME} (${TUNNEL_STATUS})"
echo -e " Config: $([ "${REMOTE_CONFIG}" = "true" ] && echo "dashboard-managed" || echo "local config")"
# Check tunnel connections
CONNS=$(echo "${tunnel}" | jq '.connections | length')
echo -e " Connections: ${CONNS}"
echo ""
done < <(echo "${TUNNELS}" | jq -c '.result[]')
Code Pack: Sigma Detection Rule
detection:
selection:
ActionType|contains:
- 'CreateTunnel'
- 'UpdateTunnel'
- 'DeleteTunnel'
- 'UpdateTunnelConfiguration'
condition: selection
fields:
- ActorEmail
- ActionType
- ResourceID
- When
5.2 Protect Tunnels with Access Policies
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.4 |
| NIST 800-53 | AC-3 |
Description
Always protect tunnel endpoints with Access policies before exposing them publicly.
Rationale
Why This Matters:
- A tunnel hostname published without an Access policy exposes the internal application directly to the entire internet
- Creating the Access application first ensures the endpoint is never reachable during the window between publishing and securing it
- Identity-based Access policies require authenticated, authorized users before any request reaches the origin service
- Unprotected tunnels are quickly discovered by automated scanners, making an Access gate the difference between private and publicly exploitable
Attack Prevented: Unauthenticated access to internal apps, exposure of internal services, automated scanning and exploitation
ClickOps Implementation
Step 1: Create Access Application First
- Before configuring tunnel hostname, create Access application
- Configure appropriate access policy
- Test policy with test users
Step 2: Then Configure Tunnel
- Add public hostname to tunnel
- Point to internal service
- Access policy automatically protects endpoint
Never expose tunnel endpoints without Access protection.
Code Pack: Terraform
resource "cloudflare_zero_trust_access_application" "tunnel_app" {
zone_id = var.cloudflare_zone_id
name = "Tunnel-Protected Application"
domain = var.app_domain
type = "self_hosted"
session_duration = "8h"
allowed_idps = [cloudflare_zero_trust_access_identity_provider.corporate_idp.id]
auto_redirect_to_identity = true
}
resource "cloudflare_zero_trust_access_policy" "tunnel_app_policy" {
account_id = var.cloudflare_account_id
name = "Allow authenticated employees via tunnel"
decision = "allow"
include = [{
group = {
id = cloudflare_zero_trust_access_group.employees.id
}
}]
require = [{
auth_method = {
auth_method = "mfa"
}
}]
session_duration = "8h"
}
Code Pack: API Script
# Cross-reference tunnel hostnames with Access applications
TUNNELS=$(cf_get "/accounts/${CF_ACCOUNT_ID}/cfd_tunnel?is_deleted=false") || {
fail "5.2 Unable to retrieve tunnels"
increment_failed
summary
exit 0
}
APPS=$(cf_get "/accounts/${CF_ACCOUNT_ID}/access/apps") || {
fail "5.2 Unable to retrieve Access applications"
increment_failed
summary
exit 0
}
ACCESS_DOMAINS=$(echo "${APPS}" | jq -r '.result[].domain // empty')
UNPROTECTED=0
while IFS= read -r tunnel; do
TUNNEL_NAME=$(echo "${tunnel}" | jq -r '.name')
TUNNEL_ID=$(echo "${tunnel}" | jq -r '.id')
# Get tunnel config to find hostnames
CONFIG=$(cf_get "/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}/configurations") 2>/dev/null || continue
HOSTNAMES=$(echo "${CONFIG}" | jq -r '.result.config.ingress[]?.hostname // empty' 2>/dev/null || true)
for hostname in ${HOSTNAMES}; do
[ -z "${hostname}" ] && continue
if echo "${ACCESS_DOMAINS}" | grep -q "${hostname}"; then
pass "5.2 Tunnel '${TUNNEL_NAME}' hostname '${hostname}' has Access protection"
else
warn "5.2 Tunnel '${TUNNEL_NAME}' hostname '${hostname}' has NO Access policy"
UNPROTECTED=$((UNPROTECTED + 1))
fi
done
done < <(echo "${TUNNELS}" | jq -c '.result[]')
5.3 Detect and Block TryCloudflare Quick Tunnel Abuse
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 4.8, 13.3 |
| NIST 800-53 | CM-7(2), SI-4 |
Description
TryCloudflare quick tunnels create an ephemeral *.trycloudflare.com hostname without requiring a Cloudflare account. Proofpoint has tracked threat actors abusing this since February 2024 to stage and deliver remote access trojans through what looks like legitimate Cloudflare infrastructure. Block outbound access to trycloudflare.com through Gateway and alert on unauthorized cloudflared execution on managed endpoints (Proofpoint threat research).
Rationale
Why This Matters:
- Quick tunnels require no Cloudflare account, no domain, and no payment, so an attacker can stand up a delivery or command-and-control endpoint in seconds with no attributable registration trail
- The resulting hostname sits on a Cloudflare domain with a valid TLS certificate, so reputation and category filtering that would flag a newly registered domain often lets the traffic through
- Proofpoint has observed campaigns using these tunnels to deliver AsyncRAT, Xworm, VenomRAT, and similar remote access trojans through malicious LNK and shortcut files that fetch payloads over the tunnel
- The same technique works in reverse for exfiltration and unauthorized remote access: an insider or an attacker who lands on an endpoint can run
cloudflaredto expose an internal service outbound, bypassing every inbound firewall rule - Because your organisation almost certainly uses named, authenticated tunnels (section 5.1) rather than anonymous quick tunnels, blocking the quick tunnel domain outright costs nothing operationally while removing a live abuse channel
Attack Prevented: RAT delivery over trusted infrastructure, command-and-control tunnelling, unauthorized outbound exposure of internal services, data exfiltration through anonymous tunnels
ClickOps Implementation
Step 1: Block the Quick Tunnel Domain in Gateway DNS
- Navigate to: Gateway → Firewall Policies → DNS
- Click Add a policy and name it “Block TryCloudflare Quick Tunnels”
- Configure the rule:
- Selector: Domain
- Operator: is
- Value: trycloudflare.com
- Set Action to Block and save
- Confirm the policy is ordered so no broader allow rule precedes it
Step 2: Add an HTTP Policy for Defence in Depth
- Navigate to: Gateway → Firewall Policies → HTTP
- Add a policy matching the Host selector against
trycloudflare.comand any subdomain - Set Action to Block so requests that bypass DNS resolution are still stopped
Step 3: Restrict cloudflared Execution on Endpoints
- In your endpoint management or EDR platform, create an application control rule permitting
cloudflaredto run only on the servers where a named tunnel is intentionally deployed - Alert on any
cloudflaredprocess starting on a user workstation - Alert on command lines containing quick tunnel invocation flags, since a legitimate named tunnel is run as a service with a credentials file rather than an ad-hoc URL flag
Step 4: Hunt for Existing Abuse
- Review Gateway DNS and HTTP logs for historical resolutions of
trycloudflare.combefore the block was applied - Review endpoint telemetry for
cloudflaredbinaries in user-writable directories such as download and temporary folders - Investigate any LNK or shortcut file execution that preceded a quick tunnel connection, which matches the documented delivery chain
Step 5: Sanction the Legitimate Path
- Confirm every business-justified tunnel is a named tunnel owned by the account and protected by an Access policy (see sections 5.1 and 5.2)
- Document the exception process for developers who previously used quick tunnels for local testing, and point them at named tunnels instead
Time to Complete: ~30 minutes for blocking, plus hunting effort
Validation & Testing
- From a WARP-enrolled test device, attempt to resolve and reach a
trycloudflare.comhostname and confirm the Gateway block page or NXDOMAIN response is returned - Confirm the block event appears in Gateway DNS logs with the correct policy name
- Run
cloudflaredon a test workstation and confirm the endpoint alert fires within your expected detection window - Verify that named tunnels serving production applications continue to function and were not affected by the block
- Re-run the historical log hunt after 30 days to confirm no further quick tunnel activity
Compliance Mappings
| Framework | Control | Requirement |
|---|---|---|
| CIS Controls v8 | 4.8 | Uninstall or disable unnecessary services on enterprise assets |
| CIS Controls v8 | 13.3 | Deploy network intrusion detection |
| NIST 800-53 Rev 5 | CM-7(2) | Prevent program execution contrary to policy |
| NIST 800-53 Rev 5 | SI-4 | System monitoring for unauthorized connections |
| SOC 2 | CC7.2 | Anomalous network activity is detected and evaluated |
6. Monitoring & Detection
6.1 Configure Logging
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 8.2 |
| NIST 800-53 | AU-2, AU-6 |
Description
Configure comprehensive logging for Zero Trust activities and integrate with SIEM for security monitoring.
Rationale
Why This Matters:
- Without comprehensive Access, Gateway, and audit logs, malicious activity and policy violations go undetected and uninvestigable
- Exporting via Logpush to a SIEM enables correlation, alerting, and long-term retention beyond the dashboard’s limited window
- Logs of admin changes, posture failures, and denied access provide the evidence needed for incident response and forensics
- Audit trails support compliance obligations and demonstrate that Zero Trust controls are operating as designed
Attack Prevented: Undetected intrusion, delayed breach discovery, repudiation, unnoticed configuration tampering
ClickOps Implementation
Step 1: Review Default Logs
- Navigate to: Logs → Access
- Review available log types:
- Access requests
- Gateway DNS
- Gateway HTTP
- Gateway Network
Step 2: Configure Log Export
- Navigate to: Settings → Logpush
- Click Create Logpush job
- Select destination:
- Splunk
- Azure Blob Storage
- Amazon S3
- Google Cloud Storage
- Configure log fields and filters
Step 3: Enable Real-Time Logs
- Navigate to: Logs → Gateway
- Review real-time activity
- Configure dashboards for monitoring
Code Pack: Terraform
resource "cloudflare_logpush_job" "access_requests" {
account_id = var.cloudflare_account_id
name = "hth-access-requests"
dataset = "access_requests"
destination_conf = var.logpush_destination
enabled = true
frequency = "high"
}
resource "cloudflare_logpush_job" "gateway_dns" {
account_id = var.cloudflare_account_id
name = "hth-gateway-dns"
dataset = "gateway_dns"
destination_conf = var.logpush_destination
enabled = true
frequency = "high"
}
resource "cloudflare_logpush_job" "gateway_http" {
account_id = var.cloudflare_account_id
name = "hth-gateway-http"
dataset = "gateway_http"
destination_conf = var.logpush_destination
enabled = true
frequency = "high"
}
resource "cloudflare_logpush_job" "gateway_network" {
account_id = var.cloudflare_account_id
name = "hth-gateway-network"
dataset = "gateway_network"
destination_conf = var.logpush_destination
enabled = true
frequency = "high"
}
Code Pack: API Script
# List all Logpush jobs and check for Zero Trust datasets
LOGPUSH=$(cf_get "/accounts/${CF_ACCOUNT_ID}/logpush/jobs") || {
fail "6.1 Unable to retrieve Logpush jobs"
increment_failed
summary
exit 0
}
JOB_COUNT=$(echo "${LOGPUSH}" | jq '.result | length')
info "6.1 Found ${JOB_COUNT} Logpush job(s)"
# Check for recommended Zero Trust datasets
RECOMMENDED_DATASETS=("access_requests" "gateway_dns" "gateway_http" "gateway_network")
MISSING_DATASETS=()
for dataset in "${RECOMMENDED_DATASETS[@]}"; do
HAS_DATASET=$(echo "${LOGPUSH}" | jq --arg ds "${dataset}" '[.result[] | select(.dataset == $ds and .enabled == true)] | length')
if [ "${HAS_DATASET}" -gt 0 ]; then
pass "6.1 Logpush configured for '${dataset}'"
else
warn "6.1 No active Logpush job for '${dataset}'"
MISSING_DATASETS+=("${dataset}")
fi
done
echo "${LOGPUSH}" | jq -r '.result[] | " - \(.name // "unnamed"): \(.dataset) → \(.destination_conf | split("://")[0]) [\(if .enabled then "enabled" else "disabled" end)]"'
Code Pack: Sigma Detection Rule
detection:
selection:
ActionType|contains:
- 'DeleteLogpushJob'
- 'UpdateLogpushJob'
condition: selection
fields:
- ActorEmail
- ActionType
- ResourceID
- When
6.2 Key Events to Monitor
| Event | Log Source | Detection Use Case |
|---|---|---|
| Access denied | Access Logs | Unauthorized access attempts |
| Policy block | Gateway DNS/HTTP | Malware/policy violations |
| Device posture fail | Access Logs | Compromised devices |
| Admin changes | Audit Logs | Unauthorized modifications |
| Tunnel disconnection | Tunnel Logs | Service availability |
| Isolation triggered | Gateway HTTP | High-risk browsing |
| API token created or rolled | Audit Logs | Unauthorized credential creation |
| trycloudflare.com resolution | Gateway DNS/HTTP | Quick tunnel abuse, RAT delivery |
| DLP profile match | Gateway HTTP | Sensitive data exfiltration |
| User risk score elevated | Risk Score / Access Logs | Account compromise, insider activity |
7. Compliance Quick Reference
SOC 2 Trust Services Criteria Mapping
| Control ID | Cloudflare Control | Guide Section |
|---|---|---|
| CC6.1 | IdP authentication | 1.1 |
| CC6.1 | MFA enforcement | 1.2 |
| CC6.1 | Scoped API tokens | 1.5 |
| CC6.1 | Account 2FA enforcement | 1.6 |
| CC6.2 | Admin roles | 1.4 |
| CC6.6 | Access policies | 2.1 |
| CC6.6 | SSH infrastructure access | 2.4 |
| CC6.7 | Gateway DLP | 3.5 |
| CC7.1 | Gateway filtering | 3.1 |
| CC7.2 | Logging | 6.1 |
| CC7.2 | User risk score gating | 2.5 |
| CC7.2 | Quick tunnel abuse detection | 5.3 |
NIST 800-53 Rev 5 Mapping
| Control | Cloudflare Control | Guide Section |
|---|---|---|
| IA-2 | IdP integration | 1.1 |
| IA-2(1) | MFA | 1.2 |
| IA-2(1) | Account member 2FA | 1.6 |
| IA-5 | API token lifetime and rotation | 1.5 |
| AC-3 | Access policies | 2.1 |
| AC-2(11) | Device posture | 2.3 |
| AC-2(12) | User risk score gating | 2.5 |
| AC-4 | Gateway DLP | 3.5 |
| AC-17 | SSH infrastructure access | 2.4 |
| SC-7 | Gateway policies | 3.1 |
| CM-7(2) | Quick tunnel blocking | 5.3 |
| AU-2 | Logging | 6.1 |
Appendix A: Plan Compatibility
| Feature | Free | Teams | Enterprise |
|---|---|---|---|
| Access (50 users) | ✅ | ✅ | ✅ |
| Gateway DNS filtering | ✅ | ✅ | ✅ |
| Gateway HTTP filtering | ❌ | ✅ | ✅ |
| Device posture | ❌ | ✅ | ✅ |
| Browser Isolation | ❌ | Add-on | ✅ |
| CASB | ❌ | Add-on | ✅ |
| Logpush | ❌ | ✅ | ✅ |
| Support | Community | Standard | Enterprise |
Appendix B: References
Official Cloudflare Documentation:
- Cloudflare Trust Hub
- Cloudflare Developer Docs
- Security Best Practices
- Zero Trust Documentation
- Access Documentation
- Gateway Documentation
- WARP Client Documentation
API Documentation:
Compliance Frameworks:
- SOC 2 Type II (Security, Confidentiality, Availability), ISO 27001:2022, ISO 27018, ISO 27701, PCI DSS Level 1 (Merchant and Service Provider), FedRAMP (In Process, Moderate Baseline) — via Cloudflare Trust Hub
Security Incidents:
- November 2023 — Nation-state actor accessed internal Atlassian systems. Using credentials stolen during the October 2023 Okta breach that Cloudflare failed to rotate, attackers accessed Cloudflare’s self-hosted Atlassian Confluence, Jira, and Bitbucket between November 14-24, 2023. No customer data or systems were impacted. Cloudflare rotated over 5,000 production credentials, reimaged all machines across its global network, and physically segmented test/staging systems. (Cloudflare Blog)
- August 2025 — Salesloft Drift supply chain compromise exposed Cloudflare’s Salesforce data. The threat actor tracked as GRUB1 abused the Salesloft Drift integration with Salesforce to access Cloudflare’s Salesforce tenant between August 12-17, 2025, exfiltrating support case data — including the text customers had typed into those cases. Cloudflare found 104 API tokens in the exposed data and rotated all of them as a precaution; no core infrastructure or services were compromised. Cloudflare disclosed the incident on September 2, 2025, disconnected Salesloft, rotated every credential shared through support cases, and moved to enforce least privilege and IP restrictions on third-party application connections. (Cloudflare Blog)
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2026-08-08 | 0.2.1 | draft | Cheat-sheet cell repair: added missing Attack Prevented line(s) to §1.1, §1.3, §2.1, §3.1, §4.3, §5.1 (no content-facts changed) | Claude Code (Fable 5) |
| 2026-08-03 | 0.2.0 | draft | Add API token/Global API Key retirement (1.5), account 2FA enforcement (1.6), Access for Infrastructure SSH (2.4), user risk score gating (2.5), Gateway DLP (3.5), TryCloudflare quick tunnel abuse detection (5.3); correct the Salesloft Drift incident entry to the August 2025 Salesforce compromise with Cloudflare’s own disclosure | Claude Code (Sonnet 5) |
| 2026-06-29 | 0.1.1 | draft | Add cheat-sheet Description and Rationale for all controls | Claude Code (Opus 4.8) |
| 2025-02-05 | 0.1.0 | draft | Initial guide with Access, Gateway, and WARP hardening | Claude Code (Opus 4.5) |
Contributing
Found an issue or want to improve this guide?
- Report outdated information: Open an issue with tag
content-outdated - Propose new controls: Open an issue with tag
new-control - Submit improvements: See Contributing Guide