Restrict Web Traffic to Cloudflare on a cPanel Server
If your websites use Cloudflare's proxy, visitors reach your server through Cloudflare, which filters attacks and hides your server's IP address. But anyone who knows or finds that IP address can still connect to your server directly and bypass Cloudflare, including its firewall rules and rate limits. This article explains how to make your cPanel server accept web traffic only from Cloudflare, without affecting email, SSL certificate renewals or your visitor logs.
These steps are for cPanel servers you manage yourself (cloud servers, virtual servers and dedicated servers) that use the CSF firewall, and need root access over SSH. Make the change during a quiet time, and test your websites afterwards.
Before you start
Check these first. If any of them doesn't apply to your server, the steps below may block visitors.
- Every website hostname is proxied by Cloudflare. In each Cloudflare zone, the A records for the websites (for example example.com and www.example.com) must show the orange cloud. Hostnames that are not proxied, such as mail.example.com, are fine, as long as nobody needs to reach a website through them.
- Cloudflare's SSL/TLS mode is Full or Full (strict), not Flexible. With Flexible, Cloudflare connects to your server over plain HTTP on port 80, and forcing HTTPS (step 4) would cause a redirect loop. You can check the mode in Cloudflare under SSL/TLS > Overview.
- Your logs show real visitor IP addresses. Without the step in Keep real visitor IP addresses, your logs, WordPress security plugins such as Wordfence, and the firewall's login protection would only see Cloudflare's addresses.
- You know which other services use ports 80 and 443. Email (ports 25, 465, 587, 993 and 995), SSH and cPanel/WHM are not affected by this change. If anything else on your server serves traffic on port 443 directly, not through Cloudflare, it will stop working.
Why port 80 stays open
SSL certificates from cPanel's AutoSSL (Let's Encrypt or Sectigo) are renewed by checking a file on your server over port 80. For hostnames proxied by Cloudflare, that check comes through Cloudflare, but for hostnames that aren't proxied, such as mail.example.com, it comes straight to your server. If port 80 were closed to everyone except Cloudflare, those certificates would stop renewing, and email programs would start showing certificate warnings within a couple of months.
So this guide restricts only port 443 to Cloudflare, and keeps port 80 open but sends every request there to the HTTPS address (step 4). Someone connecting directly over HTTP only gets a redirect back to your website's address, which goes through Cloudflare.
Step 1: Back up the firewall settings
cp -p /etc/csf/csf.conf /etc/csf/csf.conf.bak cp -p /etc/csf/csf.allow /etc/csf/csf.allow.bak
Step 2: Allow Cloudflare to reach port 443 only
Cloudflare publishes its IP ranges at https://www.cloudflare.com/ips/. Each range needs to be allowed on port 443 only, using CSF's port-specific rules in /etc/csf/csf.allow, one line per range:
tcp|in|d=443|s=173.245.48.0/20 tcp|in|d=443|s=2400:cb00::/32
The easiest way is to use the script in Keep the Cloudflare IP list up to date below: run it once and it writes all the current ranges for you, and then keeps them current.
Don't add Cloudflare's ranges as plain entries (for example a line with just 173.245.48.0/20). A plain entry in csf.allow opens every port on your server to that range, including SSH and WHM, and stops the firewall from blocking login attacks that come from it. Anyone can send traffic from Cloudflare's ranges through Cloudflare's services, so this would weaken your server. If you've added Cloudflare's ranges that way before, remove those lines.
Step 3: Close port 443 to everyone else
- Open /etc/csf/csf.conf.
- Remove 443 from the TCP_IN line, and from the TCP6_IN line if your server uses IPv6. Leave 80 in both.
- Restart the firewall:
csf -r
Step 4: Send all HTTP traffic to HTTPS
In each cPanel account, go to Domains, and turn on Force HTTPS Redirect for every website domain. As root, you can also do this for a whole account at once (replace CPUSER and the domain list):
uapi --user=CPUSER SSL toggle_ssl_redirect_for_domains domains=example.com,example.net state=1
cPanel's redirect still allows AutoSSL's certificate checks through, so certificates keep renewing.
Keep real visitor IP addresses
When traffic comes through Cloudflare, your server sees Cloudflare's IP address instead of the visitor's. Apache's mod_remoteip module replaces it with the real visitor address that Cloudflare sends in the CF-Connecting-IP header. Create /etc/apache2/conf.d/cloudflare-remoteip.conf:
<IfModule remoteip_module>
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxy 173.245.48.0/20
RemoteIPTrustedProxy 103.21.244.0/22
(one RemoteIPTrustedProxy line for each Cloudflare range)
</IfModule>
Then test and restart Apache:
apachectl -t && /scripts/restartsrv_httpd
The script below also keeps these lines up to date. If your server already has this set up in a different file, set APACHE_CF in the script to that file.
Step 5: Check that it works
From a computer that is not behind Cloudflare, replace example.com and 203.0.113.10 with your domain and your server's IP address:
- Direct HTTPS should time out:
curl -k --max-time 10 --resolve example.com:443:203.0.113.10 https://example.com/
- Direct HTTP should return a redirect (301) to https://example.com/:
curl -I --resolve example.com:80:203.0.113.10 http://example.com/
- Your websites still load normally through their normal addresses.
- Your access logs and security plugins still show real visitor IP addresses.
- Email programs still connect, and SSH and WHM still work.
- In WHM, SSL/TLS > Manage AutoSSL, run a check for each account and confirm no domains fail validation.
Keep the Cloudflare IP list up to date
Cloudflare rarely changes its IP ranges, but when it does, the new ranges must be added or some visitors won't reach your websites. This script downloads Cloudflare's current list every day and, only when it has changed, rewrites the Cloudflare rules in csf.allow (port 443 only) and the RemoteIPTrustedProxy lines in your Apache file, tests the Apache configuration, and reloads the firewall and Apache. If anything fails, it puts the previous files back and emails root. It never changes your other csf.allow entries.
Save it as /usr/local/sbin/cloudflare-ips-update.sh:
#!/bin/bash
# cloudflare-ips-update.sh - sync Cloudflare ranges into CSF (port-specific) and mod_remoteip
set -u
PORTS="443" # ports Cloudflare may reach, e.g. "80 443"
CSF_ALLOW=/etc/csf/csf.allow
APACHE_CF=/etc/apache2/conf.d/cloudflare-remoteip.conf
STATE=/etc/csf/cloudflare-ips.current
LOG=/var/log/cloudflare-ips-update.log
MAILTO=root
BEGIN="# BEGIN cloudflare-ips-update (managed - do not edit)"
END="# END cloudflare-ips-update"
log(){ echo "$(date '+%F %T') $*" >> "$LOG"; }
alert(){ log "ALERT: $1"; echo "$1" | mail -s "cloudflare-ips-update on $(hostname): $2" "$MAILTO" 2>/dev/null; }
exec 9>/var/run/cloudflare-ips-update.lock
flock -n 9 || { log "already running"; exit 0; }
TMP=$(mktemp -d) || exit 1
trap 'rm -rf "$TMP"' EXIT
curl -fsS --max-time 30 https://www.cloudflare.com/ips-v4 -o "$TMP/v4" || { alert "download of ips-v4 failed" "download failed"; exit 1; }
curl -fsS --max-time 30 https://www.cloudflare.com/ips-v6 -o "$TMP/v6" || { alert "download of ips-v6 failed" "download failed"; exit 1; }
# keep only valid CIDRs; refuse suspiciously short lists
grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$' "$TMP/v4" | sort -u > "$TMP/v4.ok"
grep -E '^[0-9a-fA-F:]+::?[0-9a-fA-F:]*/[0-9]{1,3}$' "$TMP/v6" | sort -u > "$TMP/v6.ok"
n4=$(wc -l < "$TMP/v4.ok"); n6=$(wc -l < "$TMP/v6.ok")
if [ "$n4" -lt 10 ] || [ "$n6" -lt 5 ]; then alert "unexpected list size (v4=$n4 v6=$n6) - nothing changed" "bad list"; exit 1; fi
cat "$TMP/v4.ok" "$TMP/v6.ok" > "$TMP/all"
if [ -f "$STATE" ] && cmp -s "$TMP/all" "$STATE" && grep -qF "$BEGIN" "$CSF_ALLOW"; then
log "no change ($n4 v4, $n6 v6)"; exit 0
fi
# backups for rollback
cp -p "$CSF_ALLOW" "$TMP/csf.allow.bak"
[ -f "$APACHE_CF" ] && cp -p "$APACHE_CF" "$TMP/apache_cf.bak"
restore(){ cp -p "$TMP/csf.allow.bak" "$CSF_ALLOW"; [ -f "$TMP/apache_cf.bak" ] && cp -p "$TMP/apache_cf.bak" "$APACHE_CF"; csf -r >/dev/null 2>&1; }
# 1) csf.allow: replace the managed block, keep everything else
{
awk -v b="$BEGIN" -v e="$END" '$0==b{skip=1;next} $0==e{skip=0;next} !skip' "$TMP/csf.allow.bak"
echo "$BEGIN"
while read -r cidr; do for p in $PORTS; do echo "tcp|in|d=$p|s=$cidr # Cloudflare"; done; done < "$TMP/all"
echo "$END"
} > "$TMP/csf.allow.new"
cp "$TMP/csf.allow.new" "$CSF_ALLOW"
# warn if plain (all-port) Cloudflare entries are still present outside the block
full=$(awk -v b="$BEGIN" -v e="$END" '$0==b{s=1} $0==e{s=0;next} !s' "$CSF_ALLOW" | grep -ciE '^[0-9a-f:.]+/[0-9]+ *# *cloudflare')
[ "$full" -gt 0 ] && alert "$full plain Cloudflare line(s) still in $CSF_ALLOW outside the managed block - remove them so Cloudflare only reaches port(s) $PORTS" "old plain entries"
# 2) Apache mod_remoteip: replace RemoteIPTrustedProxy lines, keep the rest of the file
if [ -f "$APACHE_CF" ]; then
if grep -q '</IfModule>' "$TMP/apache_cf.bak"; then
{ grep -v -e '^[[:space:]]*RemoteIPTrustedProxy' -e '</IfModule>' "$TMP/apache_cf.bak"
while read -r cidr; do echo " RemoteIPTrustedProxy $cidr"; done < "$TMP/all"
echo "</IfModule>"; } > "$TMP/apache_cf.new"
else
{ grep -v '^[[:space:]]*RemoteIPTrustedProxy' "$TMP/apache_cf.bak"
while read -r cidr; do echo "RemoteIPTrustedProxy $cidr"; done < "$TMP/all"; } > "$TMP/apache_cf.new"
fi
cp "$TMP/apache_cf.new" "$APACHE_CF"
if ! /usr/sbin/apachectl -t >/dev/null 2>&1; then restore; alert "apache config test failed - restored previous files" "apache test failed"; exit 1; fi
fi
# 3) reload
if ! csf -r >/dev/null 2>&1; then restore; alert "csf -r failed - restored previous files" "csf reload failed"; exit 1; fi
[ -f "$APACHE_CF" ] && /scripts/restartsrv_httpd >/dev/null 2>&1
cp "$TMP/all" "$STATE"
log "updated: $n4 v4, $n6 v6 ranges, ports: $PORTS"
alert "Cloudflare ranges updated ($n4 v4, $n6 v6); $CSF_ALLOW and $APACHE_CF rewritten" "ranges updated"
exit 0
Install it, run it once, and schedule it daily:
chmod 700 /usr/local/sbin/cloudflare-ips-update.sh /usr/local/sbin/cloudflare-ips-update.sh tail -3 /var/log/cloudflare-ips-update.log echo '23 4 * * * root /usr/local/sbin/cloudflare-ips-update.sh' > /etc/cron.d/cloudflare-ips-update
After the first run, /etc/csf/csf.allow contains one tcp|in|d=443 line for each Cloudflare range between the BEGIN and END markers. Change MAILTO if root's email isn't read.
Undoing the change
- Restore the backups from step 1 and restart the firewall:
cp -p /etc/csf/csf.conf.bak /etc/csf/csf.conf cp -p /etc/csf/csf.allow.bak /etc/csf/csf.allow csf -r
- Turn Force HTTPS Redirect back off if you don't want it.
- Remove /etc/cron.d/cloudflare-ips-update to stop the daily update.
Going further: Authenticated Origin Pulls
The firewall restriction checks only that a connection comes from Cloudflare's network. Cloudflare's network is shared by all of its customers, so someone with their own Cloudflare account could still send requests to your server through it. Authenticated Origin Pulls closes that gap: Cloudflare presents a TLS client certificate on every connection to your server, and Apache refuses HTTPS connections that don't present it.
There are two ways to set it up:
- With Cloudflare's shared certificate: one setting per zone, but the certificate is the same for every Cloudflare customer, so it adds little to the firewall restriction.
- With your own certificate (recommended): you upload your own client certificate to your Cloudflare zones, and your server trusts only that certificate, so only your zones can connect.
1. Create your own certificate
mkdir -p /etc/pki/cf-aop && cd /etc/pki/cf-aop && umask 077 openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout aop-ca.key -out aop-ca.pem -subj "/CN=Origin Pull CA" openssl req -newkey rsa:2048 -nodes -keyout aop-client.key -out aop-client.csr -subj "/CN=Cloudflare origin pull" openssl x509 -req -in aop-client.csr -CA aop-ca.pem -CAkey aop-ca.key -CAcreateserial -days 825 -sha256 -out aop-client.pem
2. Turn it on in Cloudflare first
For each zone, upload aop-client.pem and aop-client.key as the zone's origin pull certificate, wait until the certificate is active, and then turn on Authenticated Origin Pulls (SSL/TLS > Origin Server in the Cloudflare dashboard; uploading your own certificate uses Cloudflare's API). See Cloudflare's documentation for Authenticated Origin Pulls for the exact steps.
Do this before step 3: once it's on, Cloudflare presents the certificate on every connection, and your server simply ignores it until you require it.
3. Require the certificate in Apache
For each website domain proxied by Cloudflare, create this file (replace CPUSER and DOMAIN). cPanel keeps files in this folder when it rebuilds the Apache configuration:
# /etc/apache2/conf.d/userdata/ssl/2_4/CPUSER/DOMAIN/cf-aop.conf SSLVerifyClient require SSLVerifyDepth 1 SSLCACertificateFile /etc/pki/cf-aop/aop-ca.pem
Then rebuild, test and restart Apache:
/scripts/rebuildhttpdconf && apachectl -t && /scripts/restartsrv_httpd
4. Check and maintain it
- Your websites still load normally through Cloudflare.
- A direct HTTPS connection to your server is refused, even when it comes from Cloudflare's IP ranges through another Cloudflare account.
- The client certificate above is valid for 825 days. Before it expires, create a new one from the same CA (the last two openssl commands) and upload it to Cloudflare.
To undo it, delete the cf-aop.conf files, run the rebuild command again, and then turn Authenticated Origin Pulls off in Cloudflare.
If you have any questions, please open a support ticket from your client area.