Originally published on kuryzhev.cloud
Why this checklist
A client of mine once showed me their Cloudflare dashboard during what they thought was a DDoS attack. Traffic graphs were flat. Zero anomalies. Meanwhile their origin server's CPU was pegged at 100% and the app was falling over every few minutes. The attacker hadn't gone through Cloudflare at all — they'd found the origin IP and were hitting it directly, completely bypassing the WAF, the rate limiting, the bot protection, everything. That's the moment I started treating cloudflare origin hardening as a separate discipline from "just turning Cloudflare on."
Here's the uncomfortable truth: proxying your DNS through Cloudflare (the orange cloud icon) does not protect your origin server by itself. It protects requests that go through Cloudflare's edge. If someone finds your real IP — via old A records in DNS history tools like SecurityTrails, via certificate transparency logs on crt.sh, via a misconfigured mail server SPF record, or just by brute-forcing common subdomains before you enabled the proxy — they can talk directly to your server. No WAF, no rate limits, no bot challenge. Just raw TCP straight to your box.
This checklist closes three specific gaps: network-level access control (who can even reach the origin's IP), request-level filtering (bots, malicious payloads, brute force), and TLS trust between the edge and the origin (so the "encrypted" connection Cloudflare shows you is actually meaningful). If you only do one of these three, you haven't done cloudflare origin hardening — you've done a fraction of it.
The checklist (numbered)
Work through these in order. Each item is independently verifiable — don't move to the next until you've confirmed the current one actually works, not just that you clicked the toggle.
-
Restrict the origin firewall to Cloudflare's published IP ranges. Pull the current lists from cloudflare.com/ips-v4 and
ips-v6, and lock down your security group / iptables to allow ports 80 and 443 only from those ranges. - Cover IPv6, not just IPv4. If your origin has an AAAA record and Cloudflare is proxying it, the IPv6 range needs the same restriction. This gets forgotten constantly.
- Restrict management ports separately. SSH (22), database ports, admin panels — none of these should sit behind "Cloudflare-only" rules since Cloudflare doesn't proxy them. Lock these to your VPN or bastion IP, full stop.
-
Verify with a direct curl test. Run
curl -H "Host: yourdomain.com" https://ORIGIN_IP/from a machine outside your office/VPN. It should time out or get refused. If it returns a 200, your firewall rules aren't scoped correctly. -
Enable Authenticated Origin Pulls. This makes your origin only accept requests carrying a client certificate signed by Cloudflare. Import Cloudflare's origin-pull CA cert to
/etc/nginx/certs/cloudflare-origin-pull-ca.pemand setssl_verify_client on;in your nginx server block. - Set SSL/TLS mode to Full (strict). "Full" accepts any certificate, including self-signed junk — it validates encryption, not identity. Only "Full (strict)" checks the cert chain against a trusted CA.
-
Issue a Cloudflare Origin CA certificate (15-year validity, free, via the dashboard under SSL/TLS > Origin Server or the
/certificatesAPI) and install it on the origin. This cert is only trusted between Cloudflare and your server — don't try to use it for anything public-facing. -
Double check "Always Use HTTPS" isn't paired with Flexible mode. Flexible + Always HTTPS is a classic redirect-loop combo (
ERR_TOO_MANY_REDIRECTS) if your origin doesn't actually serve HTTPS. - Turn on Bot Fight Mode (free tier, coarse) or Super Bot Fight Mode (Pro/Business, JS-based detection) depending on budget. Enterprise gets ML-based Bot Management if you need it.
- Add WAF managed rules and set them to "Log" first, not "Block." Review false positives for at least a week before flipping the switch.
- Add rate limiting on sensitive endpoints — login, password reset, API auth — separate from general WAF rules. Budget for this; Free/Pro plans cap you around 10 rate limit rules as of 2024.
-
Explicitly verify WAF coverage for WebSocket and gRPC traffic. Some managed rulesets don't inspect
Upgrade: websocketrequests by default, leaving a quiet gap.
Here's a Terraform module that codifies most of the above so it isn't a one-time dashboard exercise:
# main.tf — Cloudflare origin hardening baseline via Terraform
# Provider version pinned to avoid v4->v5 breaking changes in zone_settings resources
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.30"
}
}
}
variable "zone_id" {
type = string
}
# 1. Force strict SSL between edge and origin — never use "flexible" in prod
resource "cloudflare_zone_settings_override" "strict_ssl" {
zone_id = var.zone_id
settings {
ssl = "strict"
always_use_https = "on"
min_tls_version = "1.2"
tls_1_3 = "on"
automatic_https_rewrites = "on"
}
}
# 2. Enable Authenticated Origin Pulls at the zone level
resource "cloudflare_authenticated_origin_pulls" "origin_pull" {
zone_id = var.zone_id
enabled = true
}
# 3. Enable Super Bot Fight Mode (requires Pro plan or higher)
resource "cloudflare_bot_management" "bots" {
zone_id = var.zone_id
fight_mode = true
using_latest_model = true
suppress_session_score = false
}
# 4. Rate limit login endpoint — 20 req/min per IP, then challenge
resource "cloudflare_rate_limit" "login_protect" {
zone_id = var.zone_id
threshold = 20
period = 60
match {
request {
url_pattern = "example.com/login*"
schemes = ["HTTPS"]
methods = ["POST"]
}
}
action {
mode = "challenge"
timeout = 600
}
disabled = false
}
# 5. Firewall rule: block anything failing the authenticated origin pull check
resource "cloudflare_ruleset" "origin_only" {
zone_id = var.zone_id
name = "reject-non-cf-origin-checks"
kind = "zone"
phase = "http_request_firewall_custom"
rules {
action = "block"
expression = "(not cf.tls_client_auth.cert_verified)"
description = "Block requests failing authenticated origin pull check"
enabled = true
}
}
Commonly missed items
Even teams that "complete" the checklist above leave doors open. I've audited enough setups to see the same four gaps repeatedly.
Stale IP ranges. Cloudflare updates its published IP list occasionally, without much announcement. If your firewall rules were hardcoded from a Terraform apply six months ago, you're either blocking legitimate Cloudflare edge nodes (causing intermittent 5xx errors that look random) or leaving gaps that an attacker could theoretically slot into. Watch out for this — it's silent until it isn't.
Port 80 left wide open. This is the mistake I see most. Teams lock 443 down to Cloudflare IPs and completely forget port 80. Result: plaintext HTTP requests reach the origin directly, bypassing the redirect-to-HTTPS logic that only exists at the edge. Your "hardened" origin is still accepting raw connections.
IPv6 origin address exposure. Same story as above but easier to miss because most people mentally model their infrastructure as IPv4-only. If your host has an AAAA record and IPv6 firewall rules weren't updated alongside IPv4, you've got an open backdoor nobody's watching.
Cert expiry with no monitoring. Cloudflare Origin CA certs last 15 years, so that's usually fine. But teams that use a public cert (Let's Encrypt, 90-day validity) on "Full (strict)" mode and never wire up renewal specifically for the origin-facing cert will eventually see error 526 — Invalid SSL Certificate. It happens quietly. No alert, no dashboard warning, just a slow bleed of failed requests until someone notices traffic dropped. I stopped using Let's Encrypt on origins behind Cloudflare after this bit a client twice — Origin CA certs with a 15-year runway remove the whole class of problem.
Also worth knowing: error 525 means the SSL handshake between edge and origin failed outright (usually a cipher mismatch or missing intermediate cert), while 526 specifically means the cert itself is invalid or expired under strict mode. Different root causes, same symptom of "site is down and Cloudflare's error page doesn't tell you why."
Automation ideas
A checklist you run once during setup rots. Cloudflare origin hardening needs to be enforced continuously, not remembered.
Codify the settings above in Terraform (see the module earlier) so SSL mode, WAF rules, and Authenticated Origin Pulls live in version control instead of dashboard clicks nobody documented. Check the Cloudflare Terraform provider docs for the current resource names — v4 renamed a few things and pinning your provider version avoids surprise breaking changes on the next terraform init.
For the IP range rot problem, run a scheduled job — GitHub Actions cron or a plain crontab entry — that pulls the current lists from ips-v4/ips-v6 and diffs them against what's in your security group. Alert on any change instead of silently applying it; you want a human to glance at what changed before it goes live.
And build this into CI as a smoke test after every deploy — fail the pipeline if the origin is directly reachable:
#!/usr/bin/env bash
# verify_origin_lockdown.sh — CI smoke test for origin hardening
# Run in pipeline after deploy; fails build if origin is directly reachable
ORIGIN_IP="203.0.113.45" # replace with real origin IP, ideally from Terraform output
DOMAIN="example.com"
echo "== Test 1: direct IP access should be refused =="
STATUS=$(curl -o /dev/null -s -w "%{http_code}" --max-time 5 \
-H "Host: ${DOMAIN}" "https://${ORIGIN_IP}/" -k || echo "TIMEOUT")
if [[ "$STATUS" == "TIMEOUT" || "$STATUS" == "000" ]]; then
echo "PASS: origin unreachable directly ($STATUS)"
else
echo "FAIL: origin responded directly with HTTP $STATUS — firewall misconfigured"
exit 1
fi
echo "== Test 2: SSL mode check via edge =="
curl -sI "https://${DOMAIN}/" | grep -i "strict-transport-security" \
&& echo "PASS: HSTS present" \
|| echo "WARN: HSTS header missing — check Always Use HTTPS + HSTS settings"
echo "== Test 3: confirm cert issuer is Cloudflare-trusted chain (origin side) =="
openssl s_client -connect "${ORIGIN_IP}:443" -servername "${DOMAIN}" /dev/null \
| openssl x509 -noout -issuer -dates
# Expected: issuer should reference "CloudFlare Origin SSL Certificate Authority"
# and dates should show >30 days remaining before "notAfter"
This turns cloudflare origin hardening from a one-time audit into something your pipeline actively guards. The nginx side of Authenticated Origin Pulls is documented in the nginx ssl module docs if you need to fine-tune ssl_verify_client beyond the basic "on" setting. For more on layering rate limiting and WAF decisions correctly, I wrote about picking between nginx-level limits and a full API gateway on kuryzhev.cloud — worth a read if you're deciding where that logic should live.
The core lesson: a green Cloudflare dashboard means nothing if your origin's real IP is one DNS history lookup away from being public knowledge. Treat the origin as its own attack surface, not just a backend behind a proxy.
Top comments (0)