ENGINEERING NOTES / DEVOPS, IAC

Harden WordPress with Cloudflare Free Plan Using Terraform

Maximize Cloudflare’s Free tier for WordPress using OpenTofu/Terraform. Learn how to provision 77+ resources, including WAF rules, Cache Rules, and security headers, via Infrastructure as Code - without spending a dime.

Harden WordPress with Cloudflare Free Plan Using Terraform

A technical deep-dive into maximizing Cloudflare’s free-tier security and performance for WordPress, managed entirely through Infrastructure as Code.

At Agrohi, we manage multiple WordPress properties. Every one of them faces the same reality: brute-force login attempts, XML-RPC abuse, comment spam bots, sensitive file probes, and the occasional credential-stuffing burst — all before breakfast.

We needed a security baseline that was repeatable across zones, free, and didn’t break payment webhooks. Cloudflare’s Free plan turned out to be far more capable than most people realize — if you know where to look.

This post walks through how we built an OpenTofu module that provisions 77 Cloudflare resources per zone on the Free plan, covering WAF rules, cache optimization, security headers, rate limiting, and more — all without spending a dollar on Cloudflare.

The Problem: WordPress Is a Target by Default

A fresh WordPress install exposes several well-known attack surfaces:

  • /wp-login.php Brute-force and credential stuffing
  • /xmlrpc.php Amplification attacks, brute-force via system.multicall
  • /wp-comments-post.php Automated spam submissions
  • /wp-config.php, .env, .git/ Sensitive file probes
  • /?author=N Username enumeration via author archives

Most WordPress hardening guides tell you to install a security plugin. That’s fine for a single site. But when you’re managing multiple zones, you need something deterministic, version-controlled, and repeatable.

The Approach: Infrastructure as Code with Cloudflare’s Free Tier

We chose OpenTofu (the open-source Terraform fork) with the Cloudflare provider v5. The architecture is simple:

root module (multi-zone)
  └── modules/wp_cloudflare_zone (reusable per-zone module)
        ├── cache_rules.tf
        ├── config_rules.tf
        ├── waf_custom.tf
        ├── rate_limit.tf
        ├── transform_rules.tf
        ├── redirect_rules.tf
        ├── zone_settings.tf
        ├── access_rules.tf
        ├── dns.tf
        └── managed_waf.tf

One terraform.tfvars file. One tofu apply. Every zone gets the same hardened baseline.

What Most People Don’t Know: The Free Plan Quota Budget

Here’s the part that surprises people. Cloudflare’s Free plan gives you far more than 5 WAF rules and 3 page rules. The full entitlement looks like this:

Resource TypeFree LimitWhat It Does
WAF Custom Rules5Block, challenge, or skip based on request attributes
Rate Limiting Rules1Throttle by IP per time window
Cache Rules10Control caching behavior per-path (replaces Page Rules)
Configuration Rules10Override zone settings per-path
Transform Rules10Modify request/response headers at the edge
Redirect Rules10URL redirects without touching your origin
Origin Rules10Override origin hostname, port, SNI
Access RulesUnlimitedIP/CIDR allowlist/blocklist

Page Rules are deprecated. Cloudflare stopped accepting new ones in January 2025. If your IaC is still uses cloudflare_page_rule, it will fail on new zones. The replacement is Cache Rules, which gives you 10 rules with expression-based matching instead of glob patterns.

Most WordPress-on-Cloudflare setups we’ve seen use maybe 15% of this capacity. We aimed for 60%+.

Layer 1: TLS & Transport Baseline

The foundation. Six cloudflare_zone_setting resources that enforce:

ssl                      = "strict"
min_tls_version          = "1.2"
tls_1_3                  = "on"
always_use_https         = "on"
automatic_https_rewrites = "on"
security_level           = "medium"

This eliminates mixed-content issues, enforces encrypted transport end-to-end, and sets a baseline challenge sensitivity. These settings persist even if you tear down your Terraform state; they’re zone-level toggles, not discrete objects.

Layer 2: WAF Custom Rules (5 of 5 Used)

The 5-rule limit is tight, so consolidation is key. We pack maximum coverage into each rule by OR-ing conditions together.

Rule 1 - Admin Perimeter Challenge:
Applies managed_challenge to /wp-login.php and /wp-admin/*, but exempts /wp-admin/admin-ajax.php and /wp-admin/admin-post.php (which WordPress themes and plugins hit legitimately from the frontend).

Rule 2 - XML-RPC Block:
Hard blocks /xmlrpc.php. If you’re using Jetpack or the WordPress mobile app, you’d toggle this off per-zone.

Rule 3 - Comment Spam Block:
Blocks direct POST to /wp-comments-post.php when the Referer header doesn’t contain the site’s own domain. Legitimate comment submissions always come from the site itself.

Rule 4 - Bad UA + Sensitive Probe Block:
A single rule that catches both:

  • 17 known-bad user-agent signatures (python, curl, sqlmap, nikto, wpscan, nuclei, etc.)
  • 10 sensitive file probe tokens (wp-config.php, .env, .git/, .htaccess, debug.log, phpinfo.php, etc.)

Empty user-agents are also caught. Since these are OR’d into one expression, they consume only 1 rule slot.

Rule 5 - Geo Challenge (optional):
Apply managed_challenge to traffic from outside a whitelist of allowed countries. Off by default because it’s aggressive, but available when needed.

The Critical Detail: Webhook Exclusions

Every single custom rule includes a webhook exclusion expression:

not (starts_with(http.request.uri.path, "/webhooks/stripe") or
     starts_with(http.request.uri.path, "/webhooks/paypal") or
     starts_with(http.request.uri.path, "/wc-api/"))

Payment callbacks from Stripe, PayPal, and Square are server-to-server. They often come with empty or non-browser user-agents. Without explicit exclusions, your WAF rules will break payment processing and cause order-state drift.

This is the single most important operational detail in the entire setup.

Layer 3: Cache Rules (6 of 10 Used)

These replace the deprecated Page Rules. We use 6 of the 10 available slots:

RuleExpressionAction
cache_bypass_wp_adminstarts_with(path, "/wp-admin/")Bypass cache
cache_bypass_wp_loginpath eq "/wp-login.php"Bypass cache
cache_bypass_webhooksWebhook path expressionsBypass cache
cache_bypass_wc_ajaxquery contains "wc-ajax="Bypass cache
cache_bypass_wp_cronpath eq "/wp-cron.php"Bypass cache
cache_static_assetsFile extensions (css, js, images, fonts)Cache with 1-day edge TTL

The static asset rule is the performance win - CSS, JS, images, and fonts get cached at Cloudflare’s edge with a 24-hour TTL and 4-hour browser TTL. This alone measurably improves TTFB for returning visitors.

Layer 4: Configuration Rules (4 of 10 Used)

Configuration Rules let you override zone-level settings on a per-path basis. We use 4:

  1. Disable BIC on webhooks: Browser Integrity Check kills server-to-server callbacks
  2. Disable email obfuscation on webhooks: Cloudflare’s email obfuscation rewrites email addresses in HTML responses; this corrupts JSON payloads that contain email fields
  3. Disable Rocket Loader on wp-admin: Rocket Loader async-wraps all <script> tags, which breaks WordPress admin JavaScript
  4. Elevate security level on wp-login: Sets security to “high” specifically on the login page, adding extra challenge sensitivity where it matters most

Layer 5: Security Response Headers (1 of 10 Transform Rules)

A single transform rule that modifies response headers on every response:

X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

It also strips the X-Powered-By header, which typically reveals your PHP version.

This is free security hardening that requires zero changes to your origin server. The headers are injected at Cloudflare’s edge.

Layer 6: Rate Limiting (1 of 1 Used)

The Free plan gives you exactly one rate-limiting rule. We make it count by covering both high-abuse POST endpoints in a single expression:

(path eq "/wp-login.php" or path eq "/wp-comments-post.php")
and method eq "POST"

Characteristics: cf.colo.id + ip.src (rate limit per source IP per colo).
Threshold: 20 requests per 10 seconds, with a 10-second block.

The period = 10 and mitigation_timeout = 10 values are enforced by validation - they’re the only values the Free plan supports.

Layer 7: Redirect Rules (1 of 10 Used)

Author enumeration prevention. WordPress exposes usernames via /?author=1, /?author=2, etc. Attackers use this to build username lists for brute-force attacks.

Our redirect rule catches any request with author= in the query string and 301-redirects it to the homepage. Simple, effective, no origin hit.

Layer 8: Payment Gateway IP Allowlist

For WooCommerce sites, we optionally create cloudflare_access_rule entries that whitelist known payment gateway source IPs:

  • **Stripe **- 15 IPs
  • PayPal - 8 CIDRs (expanded to /24 blocks for Cloudflare compatibility)
  • Square - 2 production + 2 optional sandbox IPs
  • Skrill - 6 IPs

The module auto-detects single IPs vs CIDRs and normalizes non-standard prefix lengths (/17/23) into exact /24 blocks, since Cloudflare access rules only accept /16 or /24 for ranges.

Important: IP allowlisting is defense-in-depth, not a substitute for webhook signature verification in your application code. Payment provider IPs can change.

The Guardrails

We enforce Free-plan safety at the validation layer, not at runtime:

# Custom rule count hard limit
lifecycle {
  precondition {
    condition     = length(local.custom_rules) <= 5
    error_message = "Free-plan custom rules exceeded (max 5)."
  }
}

# Rate limit period must be 10 (Free plan constraint)
validation {
  condition     = var.login_rate_limit.period == 10
  error_message = "Free-plan rate limiting requires period = 10."
}

If you accidentally enable too many optional custom rules (geo challenge + geo block + everything else), tofu plan fails with a clear message before touching the API.

What We Left on the Table

We’re deliberately not using:

  • Origin Rules (10 available): Useful for host header rewriting in multi-site setups, but not needed for standard WordPress
  • Request Header Transform Rules (10 available): Could add X-Real-IP forwarding or custom headers, reserved for site-specific needs
  • Managed WAF: Cloudflare provides a free managed ruleset (efb7b8c949ac4650a09736fc376e9aee) with basic OWASP and WordPress-specific protections. We keep it opt-in because it can cause false positives with some plugins

The Result

One tofu apply, 77 resources per zone:

Plan: 77 to add, 0 to change, 0 to destroy.

Every WordPress zone gets:

  • ✅ Strict TLS + forced HTTPS at the edge
  • ✅ Managed challenge on admin endpoints
  • ✅ XML-RPC blocked
  • ✅ Comment spam filtered
  • ✅ 17 scanner user-agents blocked
  • ✅ 10 sensitive file probes blocked
  • ✅ Login + comment rate limiting
  • ✅ Cache bypass on dynamic paths
  • ✅ Static asset edge caching
  • ✅ Security response headers (no origin changes)
  • ✅ Author enumeration prevented
  • ✅ Payment webhook paths excluded from all security rules
  • ✅ Payment gateway IPs allowlisted

All on the Free plan. All version-controlled. All repeatable across zones.

Getting Started

The module is designed for multi-zone use. A minimal terraform.tfvars:

zones = {
  my_site = {
    zone_id   = "your-zone-id"
    zone_name = "yourdomain.com"

    dns_records = [{
      name    = "@"
      type    = "A"
      content = "your.server.ip"
      proxied = true
    }]

    payment_webhook_paths = [
      "/webhooks/stripe",
      "/wc-api/"
    ]
  }
}

Everything else has sensible defaults. Run tofu init && tofu plan and review.

Key Takeaways

  1. Cloudflare’s Free plan is massively underutilized. Most setups use 3 Page Rules and call it done. You have 50+ rule slots across 7 rule types.
  2. Page Rules are dead. Migrate to Cache Rules now or your IaC will break on new zones.
  3. Webhook safety is non-negotiable. Every security rule must exclude payment callback paths.
  4. Transform Rules are free security headers. X-Content-Type-Options, X-Frame-Options, Referrer-Policy are all injected at the edge, zero origin changes.
  5. IaC makes multi-zone manageable. One module, one apply, deterministic state across every property.

This setup powers the WordPress infrastructure of Agrohi’s Clients. We’re a media and technology group focused on building resilient, cost-effective web properties. If you’re managing WordPress at scale and want to talk shop, reach out.

A FRESH PERSPECTIVE ON YOUR CLOUD

Great engineering starts
with a good conversation.

Let’s talk about what’s working, what’s slowing you down, and what comes next.

Talk to an engineer ↗