v0.1.0-draft AI Drafted

Cloudflare Zero Trust Hardening Guide

Security Last updated: 2025-02-05

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 (Baseline): Essential controls for all organizations
  • L2 (Hardened): Enhanced controls for security-sensitive environments
  • L3 (Maximum Security): 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

  1. Authentication & Access Controls
  2. Access Application Policies
  3. Gateway Security Policies
  4. WARP Client Hardening
  5. Tunnel Security
  6. Monitoring & Detection
  7. Compliance Quick Reference

1. Authentication & Access Controls

1.1 Configure Identity Provider Integration

Profile Level: L1 (Baseline)

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

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

  1. Navigate to: Zero Trust DashboardSettingsAuthentication
  2. Click Add new
  3. Select your IdP type:
    • Okta, Azure AD, OneLogin: Use preconfigured templates
    • Generic OIDC/SAML: Manual configuration
  4. 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)

  1. In Okta Admin: ApplicationsCreate App Integration
  2. Select OIDC - Web Application
  3. Configure:
    • Sign-in redirect: https://<team-name>.cloudflareaccess.com/cdn-cgi/access/callback
    • Sign-out redirect: https://<team-name>.cloudflareaccess.com
  4. Assign users/groups
  5. Copy Client ID and Secret to Cloudflare

Step 3: Test Authentication

  1. In Cloudflare, click Test on IdP configuration
  2. Verify successful authentication
  3. Enable the IdP for production use

Time to Complete: ~45 minutes


Code Pack: Terraform
hth-cloudflare-1.01-configure-identity-provider.tf View source on GitHub ↗
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
hth-cloudflare-1.01-configure-identity-provider.sh View source on GitHub ↗
# 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
hth-cloudflare-1.01-configure-identity-provider.yml View source on GitHub ↗
detection:
    selection:
        ActionType|contains:
            - 'DeleteAccessIdentityProvider'
            - 'UpdateAccessIdentityProvider'
    condition: selection
fields:
    - ActorEmail
    - ActionType
    - ResourceID
    - When

1.2 Configure Multi-Factor Authentication

Profile Level: L1 (Baseline)

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.

ClickOps Implementation

Option A: Enforce MFA via IdP (Recommended)

  1. Configure MFA requirement in your identity provider
  2. Create IdP policy requiring MFA for Cloudflare application
  3. All Access authentications will require MFA

Option B: Cloudflare Access Policy Requirement

  1. In Access application policy, add requirement:
    • Rule type: Require
    • Selector: Login Methods
    • Value: Select IdPs with MFA configured
  2. Optionally add additional authentication factor via policy

Code Pack: Terraform
hth-cloudflare-1.02-configure-mfa.tf View source on GitHub ↗
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
hth-cloudflare-1.02-configure-mfa.sh View source on GitHub ↗
# 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 (Baseline)

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

ClickOps Implementation

Step 1: Configure Enrollment Policies

  1. Navigate to: SettingsWARP ClientDevice enrollment permissions
  2. Click ManageAdd a rule
  3. Configure enrollment restrictions:
    • Emails ending in: @yourdomain.com
    • Identity provider groups: Specific groups only
    • Country: Allowed countries only

Step 2: Require IdP Authentication

  1. In enrollment rule, require authentication via IdP
  2. Add additional conditions:
    • Specific IdP login method (e.g., Okta with MFA)
    • Geographic restrictions
  3. Save rule

Time to Complete: ~20 minutes


Code Pack: Terraform
hth-cloudflare-1.03-harden-device-enrollment.tf View source on GitHub ↗
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
hth-cloudflare-1.03-harden-device-enrollment.sh View source on GitHub ↗
# 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 (Baseline)

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.

ClickOps Implementation

Step 1: Review Member Access

  1. Navigate to: Cloudflare DashboardManage AccountMembers
  2. Review current member roles
  3. Document Super Administrator assignments

Step 2: Implement Least Privilege

  1. 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
  2. Assign appropriate roles per responsibility
  3. Remove unnecessary Super Administrator access

Code Pack: Terraform
hth-cloudflare-1.04-configure-admin-roles.tf View source on GitHub ↗
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
hth-cloudflare-1.04-configure-admin-roles.sh View source on GitHub ↗
# 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
hth-cloudflare-1.04-configure-admin-roles.yml View source on GitHub ↗
detection:
    selection:
        ActionType: 'UpdateMember'
    filter_role:
        ActionMetadata|contains: 'Super Administrator'
    condition: selection and filter_role
fields:
    - ActorEmail
    - ActionType
    - ResourceID
    - When

2. Access Application Policies

2.1 Create Secure Application Policies

Profile Level: L1 (Baseline)

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

ClickOps Implementation

Step 1: Add Application

  1. Navigate to: AccessApplications
  2. Click Add an application
  3. Select application type:
    • Self-hosted: Applications behind Cloudflare Tunnel
    • SaaS: Third-party SaaS applications
    • Private network: Internal IP ranges

Step 2: Configure Application Settings

  1. Enter application details:
    • Name: Descriptive application name
    • Domain: Application URL
    • Session duration: 24 hours (adjust as needed)

Step 3: Create Access Policy

  1. Click Add a policy
  2. 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)

  1. Add additional require rules:
    • WARP: Require WARP client
    • Device Posture: Require compliant device
    • Location: Restrict to specific countries
  2. Add block rules for exceptions if needed

Time to Complete: ~30 minutes per application


Code Pack: Terraform
hth-cloudflare-2.01-create-access-policies.tf View source on GitHub ↗
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
hth-cloudflare-2.01-create-access-policies.sh View source on GitHub ↗
# 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
hth-cloudflare-2.01-create-access-policies.yml View source on GitHub ↗
detection:
    selection:
        ActionType|contains:
            - 'DeleteAccessPolicy'
            - 'DeleteAccessApplication'
    condition: selection
fields:
    - ActorEmail
    - ActionType
    - ResourceID
    - When

2.2 Require WARP for Application Access

Profile Level: L2 (Hardened)

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.

ClickOps Implementation

Step 1: Enable WARP Requirement in Policy

  1. Edit Access application policy
  2. Add Require rule:
    • Selector: Require WARP
    • Value: Enabled
  3. Save policy

Step 2: Configure WARP-Only Access

  1. For sensitive applications, block non-WARP access
  2. This ensures all traffic passes through Gateway for inspection

Code Pack: Terraform
hth-cloudflare-2.02-require-warp.tf View source on GitHub ↗
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
hth-cloudflare-2.02-require-warp.sh View source on GitHub ↗
# 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 (Hardened)

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.

ClickOps Implementation

Step 1: Create Device Posture Rules

  1. Navigate to: SettingsWARP ClientDevice posture
  2. Click Add new
  3. 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)

  1. Add checks for security tools:
    • CrowdStrike running
    • Carbon Black installed
    • Custom certificate present

Step 3: Apply to Access Policy

  1. Edit application Access policy
  2. Add posture checks as Require rules
  3. Block access if checks fail

Code Pack: Terraform
hth-cloudflare-2.03-configure-device-posture.tf View source on GitHub ↗
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
hth-cloudflare-2.03-configure-device-posture.sh View source on GitHub ↗
# 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))"'

3. Gateway Security Policies

3.1 Configure DNS Filtering

Profile Level: L1 (Baseline)

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

ClickOps Implementation

Step 1: Create DNS Policy

  1. Navigate to: GatewayFirewall PoliciesDNS
  2. Click Add a policy
  3. Configure blocking rules:

Step 2: Block Security Threats

  1. Create rule: “Block Security Threats”
  2. Configure:
    • Selector: Security Categories
    • Operator: in
    • Value: Malware, Phishing, Spyware, Botnet, Cryptomining, Command and Control
    • Action: Block
  3. Save

Step 3: Block Content Categories (Policy)

  1. Create additional rules for policy enforcement:
    • Adult Content
    • Gambling
    • Illegal Activities
  2. Configure action: Block or Override (with warning)

Time to Complete: ~30 minutes


Code Pack: Terraform
hth-cloudflare-3.01-configure-dns-filtering.tf View source on GitHub ↗
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
hth-cloudflare-3.01-configure-dns-filtering.sh View source on GitHub ↗
# 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
hth-cloudflare-3.01-configure-dns-filtering.yml View source on GitHub ↗
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 (Baseline)

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.

ClickOps Implementation

Step 1: Create HTTP Policy

  1. Navigate to: GatewayFirewall PoliciesHTTP
  2. Click Add a policy

Step 2: Block Malicious Content

  1. Create rule: “Block Malware Downloads”
  2. Configure:
    • Selector: Content Categories
    • Operator: in
    • Value: Malware, Botnet
    • Action: Block

Step 3: Inspect File Downloads (L2)

  1. Create rule for file inspection
  2. Configure AV scanning for downloads
  3. Block or quarantine detected threats

Code Pack: Terraform
hth-cloudflare-3.02-configure-http-filtering.tf View source on GitHub ↗
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
hth-cloudflare-3.02-configure-http-filtering.sh View source on GitHub ↗
# 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 (Hardened)

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.

ClickOps Implementation

Step 1: Create Network Policy

  1. Navigate to: GatewayFirewall PoliciesNetwork
  2. Click Add a policy

Step 2: Block Risky Protocols

  1. Create rules blocking:
    • Known malicious ports
    • Tunneling protocols (if not allowed)
    • P2P protocols
  2. Configure action: Block

Step 3: Control Private Network Access

  1. 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
hth-cloudflare-3.03-configure-network-policies.tf View source on GitHub ↗
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
hth-cloudflare-3.03-configure-network-policies.sh View source on GitHub ↗
# 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 (Maximum Security)

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.

Prerequisites

  • Browser Isolation add-on license

ClickOps Implementation

Step 1: Create Isolation Policy

  1. Navigate to: GatewayFirewall PoliciesHTTP
  2. Create rule with Action: Isolate
  3. Configure targets:
    • Uncategorized domains
    • Newly registered domains
    • High-risk categories

Step 2: Configure Isolation Settings

  1. In Settings → Browser Isolation
  2. Configure:
    • Disable copy/paste: For sensitive sites
    • Disable printing: For sensitive sites
    • Disable uploads/downloads: Based on policy

Code Pack: Terraform
hth-cloudflare-3.04-enable-browser-isolation.tf View source on GitHub ↗
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
hth-cloudflare-3.04-enable-browser-isolation.sh View source on GitHub ↗
# 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
}

4. WARP Client Hardening

4.1 Configure WARP Client Settings

Profile Level: L1 (Baseline)

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.

ClickOps Implementation

Step 1: Access WARP Settings

  1. Navigate to: SettingsWARP Client
  2. Click Manage under Global settings

Step 2: Configure Global Settings

  1. Auto connect: Enable (reconnect after disconnection)
  2. Captive portal detection: Enable (for WiFi networks)
  3. Mode switch: Configure default mode (Gateway with WARP)

Step 3: Configure Lock Settings (L2)

  1. Lock WARP switch: Enable (prevent user disable)
  2. Allow admin override: Enable with codes (for troubleshooting)
  3. Disable for WiFi: Configure trusted network exception

Code Pack: Terraform
hth-cloudflare-4.01-configure-warp-settings.tf View source on GitHub ↗
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
hth-cloudflare-4.01-configure-warp-settings.sh View source on GitHub ↗
# 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 (Hardened)

Framework Control
CIS Controls 4.1
NIST 800-53 CM-7

Description

Lock WARP client to prevent users from disabling Zero Trust protection.

ClickOps Implementation

Step 1: Enable Lock Settings

  1. Navigate to: SettingsWARP ClientDevice settings
  2. Create or edit device profile
  3. Enable Lock WARP switch

Step 2: Configure Override Codes (Optional)

  1. Enable Allow admin override codes
  2. Admins can generate temporary disable codes
  3. Codes can be time-limited

Step 3: Configure Service Mode

  1. Set Service mode: Gateway with WARP
  2. This ensures all traffic is filtered
  3. Alternative modes available for specific needs

Code Pack: Terraform
hth-cloudflare-4.02-lock-warp-client.tf View source on GitHub ↗
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
hth-cloudflare-4.02-lock-warp-client.sh View source on GitHub ↗
# 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
hth-cloudflare-4.02-lock-warp-client.yml View source on GitHub ↗
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 (Hardened)

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

ClickOps Implementation

Step 1: Access Split Tunnel Settings

  1. Navigate to: SettingsWARP ClientDevice settings
  2. Select device profile
  3. Click Configure under Split Tunnels

Step 2: Configure Minimum Exceptions

  1. Mode: Exclude IPs and domains (default is include all)
  2. Add only necessary exceptions:
    • Video conferencing (Zoom, Teams IPs)
    • Local network access (RFC1918)
  3. Document each exception

Step 3: Prefer Include Mode (L3)

  1. For maximum security, use Include mode
  2. Only specified traffic goes through WARP
  3. Everything else uses local network

Code Pack: Terraform
hth-cloudflare-4.03-configure-split-tunnels.tf View source on GitHub ↗
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
hth-cloudflare-4.03-configure-split-tunnels.sh View source on GitHub ↗
# 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 (Baseline)

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

ClickOps Implementation

Step 1: Create Tunnel

  1. Navigate to: AccessTunnels
  2. Click Create a tunnel
  3. Name the tunnel descriptively
  4. Install cloudflared on origin server

Step 2: Configure Public Hostname

  1. Add public hostname routing
  2. Configure:
    • Subdomain: app.yourdomain.com
    • Service: http://localhost:8080
  3. Always create Access policy before exposing

Step 3: Secure Tunnel Credentials

  1. Tunnel token should be treated as secret
  2. Store securely (vault, secrets manager)
  3. Rotate if compromised

Code Pack: Terraform
hth-cloudflare-5.01-secure-tunnel-config.tf View source on GitHub ↗
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
hth-cloudflare-5.01-secure-tunnel-config.sh View source on GitHub ↗
# 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
hth-cloudflare-5.01-secure-tunnel-config.yml View source on GitHub ↗
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 (Baseline)

Framework Control
CIS Controls 6.4
NIST 800-53 AC-3

Description

Always protect tunnel endpoints with Access policies before exposing them publicly.

ClickOps Implementation

Step 1: Create Access Application First

  1. Before configuring tunnel hostname, create Access application
  2. Configure appropriate access policy
  3. Test policy with test users

Step 2: Then Configure Tunnel

  1. Add public hostname to tunnel
  2. Point to internal service
  3. Access policy automatically protects endpoint

Never expose tunnel endpoints without Access protection.


Code Pack: Terraform
hth-cloudflare-5.02-protect-tunnels-with-access.tf View source on GitHub ↗
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
hth-cloudflare-5.02-protect-tunnels-with-access.sh View source on GitHub ↗
# 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[]')

6. Monitoring & Detection

6.1 Configure Logging

Profile Level: L1 (Baseline)

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.

ClickOps Implementation

Step 1: Review Default Logs

  1. Navigate to: LogsAccess
  2. Review available log types:
    • Access requests
    • Gateway DNS
    • Gateway HTTP
    • Gateway Network

Step 2: Configure Log Export

  1. Navigate to: SettingsLogpush
  2. Click Create Logpush job
  3. Select destination:
    • Splunk
    • Azure Blob Storage
    • Amazon S3
    • Google Cloud Storage
  4. Configure log fields and filters

Step 3: Enable Real-Time Logs

  1. Navigate to: LogsGateway
  2. Review real-time activity
  3. Configure dashboards for monitoring

Code Pack: Terraform
hth-cloudflare-6.01-configure-logging.tf View source on GitHub ↗
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
hth-cloudflare-6.01-configure-logging.sh View source on GitHub ↗
# 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
hth-cloudflare-6.01-configure-logging.yml View source on GitHub ↗
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

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.2 Admin roles 1.4
CC6.6 Access policies 2.1
CC7.1 Gateway filtering 3.1
CC7.2 Logging 6.1

NIST 800-53 Rev 5 Mapping

Control Cloudflare Control Guide Section
IA-2 IdP integration 1.1
IA-2(1) MFA 1.2
AC-3 Access policies 2.1
AC-2(11) Device posture 2.3
SC-7 Gateway policies 3.1
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:

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)
  • March 2025 — Third-party vendor breaches (Salesloft/Drift) exposed limited customer data. Attackers compromised Cloudflare’s marketing vendors, gaining indirect access to a subset of customer information. Cloudflare’s core infrastructure was not affected. (The Register)

Changelog

Date Version Maturity Changes Author
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?