Cloudflare’s free plan can replace a security suite and a caching plugin at once, provided you set it up in the right order. Here’s the order, with the two steps most guides skip.
Cloudflare is a reverse proxy: visitors connect to Cloudflare’s network, and Cloudflare fetches pages from your server (the “origin”) on their behalf. That one hop gets you DNS, HTTPS, a web application firewall, rate limiting, and a global cache, all before a request touches PHP. For a self-hosted WordPress site it’s the biggest upgrade you can make, and the free plan covers all of it.
Two things this guide does that most don’t: it locks the origin so attackers can’t just go around Cloudflare, and it restores the real visitor IP so your rate limits count attackers instead of counting Cloudflare. Skip either one and the rest doesn’t do much.
Before you start
- Access to your domain registrar (to change nameservers).
- A host where you can install a certificate and edit the firewall: a VPS with SpinupWP, or any host with SSH. On shared hosting you can do everything except the real-IP and origin-lock sections, which need server access; add Fuse Limit Logins to cover the gap.
- A Cloudflare account with two-factor turned on before you add a domain. This account will control your DNS; it deserves a hardware key if you have one.
- About an hour. Do it on a quiet afternoon, not launch night.
The order matters
Certificate first, then proxy, then lock the origin, then verify the real IP. Locking the firewall before the certificate is valid takes the site down; proxying before the certificate is valid gets you Cloudflare’s 525/526 error pages. Go in order.
Move DNS to Cloudflare, proxy off
- Add a domain in the Cloudflare dashboard, choose the Free plan. Cloudflare scans and imports your existing records; check every one against your registrar’s DNS page. It misses records more often than it admits.
- Set every web record (
@,www) to DNS only (grey cloud) for now. - Check your MX and mail-related records (
mail., SPF/DKIM TXT records, DMARC). Mail records must stay grey. Cloudflare doesn’t proxy email. If your mail runs on the same server as your website, that record permanently reveals your server’s IP; the origin lock below is what actually protects you in that case. - Remove anything stale: old A records,
ftp.,cpanel.,direct.. Every unproxied record pointing at your server gives away its address. - At your registrar, replace the nameservers with the two Cloudflare gives you. Propagation takes minutes to a few hours. Cloudflare emails you when the zone is active.
- While you’re here: DNS → Settings → DNSSEC → Enable, and add the DS record it gives you at your registrar. It stops DNS responses for your domain from being forged.
Install an origin certificate
There are two connections now: visitor → Cloudflare, and Cloudflare → your server. Cloudflare handles the first automatically. The second is where sites go down.
Most hosts use Let’s Encrypt, which proves you own the domain by fetching a file from your server over plain HTTP every 90 days. Once Cloudflare is proxying, that check can fail to reach the origin: the certificate lapses, Cloudflare can’t verify the origin, and visitors see an error while the host’s dashboard insists everything is fine. We’ve been paged for exactly this.
The permanent fix is a Cloudflare Origin Certificate: a 15-year certificate that only Cloudflare trusts. Since every visitor comes through Cloudflare, that’s all the trust you need.
- SSL/TLS → Origin Server → Create Certificate. Keep the defaults (RSA,
yourdomain.caand*.yourdomain.ca, 15 years). Copy both the certificate and the private key: the key is shown once. - Install them at the host. In SpinupWP: site → SSL → Custom certificate, paste both, and turn off the Let’s Encrypt auto-renewal for that site. On other hosts, the option is usually called “custom SSL” or “upload certificate”; on bare nginx it’s two files and two lines in the server block.
- SSL/TLS → Overview → Full (Strict). Cloudflare will now refuse to talk to the origin unless the certificate is valid, which, being Cloudflare’s own, it is.
- SSL/TLS → Edge Certificates: Always Use HTTPS on · Automatic HTTPS Rewrites on · Minimum TLS Version 1.2 · TLS 1.3 on. Leave HSTS for the end.
Nothing will ever need renewing again, and because origin certificates aren’t published in public certificate logs, you’ve closed one more way of discovering your server’s IP.
* Already stuck in the Let’s Encrypt loop with an expired certificate? Grey-cloud the records, wait a minute, reissue at the host, orange-cloud them again. Then do the steps above so it’s the last time.
Turn the proxy on
- DNS → Records: flip
@andwwwto Proxied (orange cloud). - Load the site in a private window. Click the padlock: the certificate should now be issued to Cloudflare (Google Trust Services or Let’s Encrypt via Cloudflare), not your host.
- Log in to wp-admin. Navigate around. Save a page. If anything redirect-loops, your WordPress URLs still say
http://; fix them in Settings → General.
Restore the real visitor IP
Every request now arrives from a Cloudflare IP address. Unless your server is told otherwise, WordPress, your logs, your form spam protection, and every per-IP limit you set up will see one visitor: Cloudflare. Block it once and you’ve blocked the world. Cloudflare sends the true address in a CF-Connecting-IP header; the server has to be told to trust it.
- SpinupWP: site → Settings → turn on the Cloudflare toggle. That’s it; it writes the nginx real-IP configuration for you.
- Plain nginx: add a
set_real_ip_fromline for each range atcloudflare.com/ips, thenreal_ip_header CF-Connecting-IP;. Reload nginx. - Apache: enable
mod_remoteipwithRemoteIPHeader CF-Connecting-IPand aRemoteIPTrustedProxyline per range. - Shared hosting: ask support whether they “restore visitor IPs behind Cloudflare”. Many do by default; some need a ticket.
Verify it. Open your site on your phone over mobile data, then look at the access log on the server (or a plugin that shows recent logins). The address should be your carrier’s, not one starting 172., 173.245., 104. or 162.158.. If it is Cloudflare’s, stop and fix this before the firewall rules, or your rate limit will throttle everyone.
Lock the origin
Proxying hides your IP from casual lookup. It doesn’t hide it from DNS history services, old certificate logs, or a single leaked record, and anyone who finds it can talk to your server directly and skip every rule you’re about to write. Two layers close that.
Firewall the server to Cloudflare’s addresses
Check your host’s panel first. SpinupWP and several others have a one-click “only allow Cloudflare” firewall option, and using it means the panel won’t later overwrite your rules. If you’re doing it by hand on Ubuntu with ufw:
# Keep your own SSH access FIRST. Lockout is the real risk here.
sudo ufw allow from YOUR.HOME.IP to any port 22 proto tcp
for ip in $(curl -s https://www.cloudflare.com/ips-v4); do
sudo ufw allow from $ip to any port 80,443 proto tcp
done
for ip in $(curl -s https://www.cloudflare.com/ips-v6); do
sudo ufw allow from $ip to any port 80,443 proto tcp
done
sudo ufw default deny incoming
sudo ufw enable
sudo ufw status numbered # delete any leftover "Anywhere -> 80/443" rule
Keep your provider’s web console open in another tab while you do this. If SSH drops, that console is how you get back in. Cloudflare’s ranges change rarely; re-run the loop a few times a year (a monthly cron is the tidy answer).
Turn on Authenticated Origin Pulls
Cloudflare’s IPs are shared by every Cloudflare customer, so in theory someone could point their Cloudflare zone at your server and walk through the firewall. Authenticated Origin Pulls closes that: Cloudflare presents a client certificate on every request, and your server refuses anyone who doesn’t.
- SSL/TLS → Origin Server → Authenticated Origin Pulls → On.
- Download Cloudflare’s origin-pull CA certificate (linked from that same page’s docs) to the server, e.g.
/etc/nginx/cloudflare/origin-pull-ca.pem. - In the site’s nginx config (SpinupWP users: the Custom Nginx Config area, never the generated files), add:
ssl_client_certificate /etc/nginx/cloudflare/origin-pull-ca.pem;ssl_verify_client on; sudo nginx -t && sudo systemctl reload nginx. Reload the site. If it loads, you’re done; if it 400s, you enabled the nginx side before the Cloudflare side; flip the toggle.
Verify the lock. From your computer: curl -sk https://YOUR.SERVER.IP -H "Host: yourdomain.ca". With the firewall on, it should hang and time out. With AOP alone, it should return a 400. Either way, direct requests to the server are refused.
Add the WordPress firewall rules
This is where Cloudflare replaces the security plugin. The free plan gives you five custom WAF rules and one rate-limiting rule, which is enough. All of these live under Security → WAF.
Rate limit the login
Rate limiting rules → Create rule. Name it “Login throttle”. Match: URI Path equals /wp-login.php and Request Method equals POST. Rate: 5 requests per 10 seconds from the same IP. Action: Block for 10 seconds (the free plan’s fixed window). Nobody types a password five times in ten seconds; bots do it five hundred times.
Add custom rules
- “Challenge login page”: URI Path equals
/wp-login.php→ Managed Challenge. Humans pass it silently or with one click; most bots never reach PHP at all. Pairs with the rate limit: the challenge thins the herd, the throttle handles whatever’s left. - “Block XML-RPC”: URI Path equals
/xmlrpc.php→ Block. It’s a legacy API that almost nothing uses and every bot scans. (If you use the Jetpack app or a remote-publishing tool, you’ll know; exempt it; everyone else, block.) - “Block probes”: URI Path contains any of
/.env,/.git,/wp-config,/.well-known/-excluded dotfiles,/vendor/,/backup. Or, simpler: URI Path starts with/.and not starts with/.well-known/→ Block. Watch the firewall events log for a week and add what you see. - “Protect admin (optional)”: URI Path starts with
/wp-admin/and not equals/wp-admin/admin-ajax.php→ Managed Challenge. If you’re in one country, add and Country not equals CA to cut noise without bothering yourself. The admin-ajax exception matters: some plugins call it from the public front end.
Paths to leave alone
WordPress talks to itself. Challenge the wrong path and the block editor stops saving, forms stop submitting, or scheduled posts stop publishing. Leave these alone, and if you enable anything broad (Bot Fight Mode, “I’m Under Attack”, Security Level High), add a top-priority Skip rule for them:
/wp-json/: the REST API the editor uses to save your pages./wp-admin/admin-ajax.php: front-end form and plugin requests./wp-cron.php: scheduled tasks, unless you’ve moved cron to the server (you should; SpinupWP does).- Any webhook path a payment or email service posts to: you’ll know if you have one.
General security settings
- Security → Settings: Security Level Medium (High is fine once your Skip rules exist) · Browser Integrity Check on · Challenge Passage 30 minutes.
- Bot Fight Mode: on, but only after the Skip rules above. It’s free, it’s effective, and it’s exactly the thing that breaks the editor if you skip that step.
- Managed rules: the free managed ruleset is on by default. The full OWASP set needs a paid plan; for a brochure site the free one plus the rules above is enough.
Optional: put wp-admin behind Cloudflare Access
If only you and one or two people ever log in, Cloudflare Access (Zero Trust, free for up to 50 users) can require an emailed one-time code before anyone even sees /wp-login.php. Attackers can’t probe a login form they can’t load.
- Zero Trust → Access → Applications → Add → Self-hosted. Domain
yourdomain.ca, pathwp-login.php; add a second path forwp-admin. - Policy: Allow · include Emails → your addresses. Session 24 hours.
- Add a Bypass policy for
/wp-admin/admin-ajax.phpso public-facing plugin requests still work.
Skip this on sites with many editors; the extra step every morning wears on people, and the rate limit already does the heavy lifting.
Tune caching and speed
Security done; now the part people come for. The guiding rule: cache static files at Cloudflare, cache pages at your server. Cloudflare caching whole HTML pages is possible, but WordPress sets cookies for logged-in users and form submissions, and getting that right is a weekend of its own. Your host’s page cache (SpinupWP’s, or whatever your host calls it) already serves anonymous visitors in milliseconds. Let Cloudflare handle everything else.
- Caching → Configuration: Caching Level Standard · Browser Cache TTL Respect Existing Headers (your server already sets long cache times on images, CSS and JS) · Always Online on. Cloudflare serves a saved copy if your server ever goes down.
- Caching → Tiered Cache: Smart Tiered Cache on. Free, and it means a visitor in Vancouver doesn’t make a Vancouver data centre fetch from Toronto if another one already has the file.
- Speed → Optimization → Content: Brotli on · Early Hints on. Rocket Loader off; it rewrites how scripts load and breaks block-editor sites in subtle ways. (Auto Minify was retired in 2024; ShortPixel and your theme already handle what it did.)
- Speed → Optimization → Protocol: HTTP/3 (QUIC) on · 0-RTT on.
- Cache Rules → Create rule, “Bypass for logged-in”: If Cookie contains
wordpress_logged_in→ Bypass cache. Cloudflare doesn’t cache HTML by default, but this rule makes the intent explicit and protects you if you ever turn page caching on at the edge.
Want Cloudflare to cache pages too? Cloudflare’s APO add-on ($5/month, via their official WordPress plugin) does it properly; it knows about WordPress cookies and purges when you publish. It’s the only edge page-caching option we’d recommend to someone who doesn’t want to write cache rules by hand. For most business sites the server cache is already enough; check your page’s load time from a phone before paying for it.
* Polish and Mirage (Cloudflare’s image optimizers) are paid-plan features. ShortPixel does the same job on your side for less. Leave them.
Finish the edge settings
- SSL/TLS → Edge Certificates → HSTS: now that HTTPS has worked for a few days, enable it. Max Age 12 months, Include subdomains on. Leave Preload off unless you’re certain every subdomain you’ll ever have is HTTPS; preload is close to irreversible.
- SSL/TLS → Edge Certificates → Certificate Transparency Monitoring: on. You get an email if anyone issues a certificate for your domain.
- Scrape Shield: Email Address Obfuscation on · Server-side Excludes on · Hotlink Protection off (it breaks legitimate embeds of your images, including your own social previews).
- Account: if you ever generate an API token (for a backup tool, a deploy script), make it a scoped token for that one zone and permission, never the Global API Key. Glance at the Audit Log a couple of times a year.
Verify the setup
curl -sI https://yourdomain.ca/ | grep -i cf-: you should seecf-rayandcf-cache-statusheaders. That’s the proxy.curl -sI https://yourdomain.ca/wp-content/themes/ollie/style.css | grep -i cf-cache-status, run twice. Second time should sayHIT.curl -sk https://YOUR.SERVER.IP -H "Host: yourdomain.ca": should time out or 400. Origin locked.- Log in from your phone; confirm the server saw your carrier’s IP (see the real-IP section).
- Type a wrong password at
/wp-login.phpsix times fast. The sixth should be a Cloudflare block page. Wait ten seconds; it clears. - Edit a page, save it, submit your contact form, and confirm both worked. If the editor spins forever, a rule is catching
/wp-json/; check Security → Events to see which. curl -sI https://yourdomain.ca/xmlrpc.php: 403. Good.
That’s a firewall, brute-force protection, a CDN and a certificate that never expires, with no plugin on your server and no monthly bill. Keep the server updated, keep two-factor on, and let the edge take the hits.
* One caveat: Cloudflare now sees every request to your site. It’s a large American company with a strong privacy record and a Canadian data-localization option on paid plans. If that trade bothers you, the alternative is doing the real-IP, lockdown, and firewall work at your own server. That’s possible, and the subject of a future guide.
Leave a Reply