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 configure your cPanel server's Apache web server to accept website traffic only from Cloudflare, and only for your own Cloudflare account, without affecting email, SSL certificate renewals or your visitor logs.
The method uses two layers:
- Firewall (IP restriction): only Cloudflare's IP addresses can connect to your server's HTTPS port (443). This blocks direct connections from everyone else.
- Origin certificate (Authenticated Origin Pulls): Cloudflare's network is shared by all of its customers, so the IP restriction alone can't tell your Cloudflare traffic apart from someone else's. With Authenticated Origin Pulls, Cloudflare presents a private certificate that only your Cloudflare zones have, and Apache refuses any HTTPS connection without it. This makes sure the traffic comes through your own Cloudflare account.
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 and access to your Cloudflare account. Make the changes during a quiet time, and test your websites after each part.
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. 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 port 443, 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.
Part 1: Allow only Cloudflare's IP addresses
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.
Part 2: Accept only your own Cloudflare account (Authenticated Origin Pulls)
Cloudflare offers Authenticated Origin Pulls with either its own shared certificate or a certificate you create. The shared certificate is the same for every Cloudflare customer, so it doesn't prove the traffic comes from your account. This guide uses your own certificate, so only your Cloudflare zones can connect.
Order matters: turn it on in Cloudflare (step 6) before requiring it in Apache (step 7). Once it's on, Cloudflare presents the certificate on every connection and your server ignores it until you require it, so nothing breaks in between. If you do it the other way round, your websites return errors through Cloudflare.
Step 5: Create your own certificate
This creates a private certificate authority (CA) that only your server trusts, and a client certificate signed by it for Cloudflare to present:
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
Keep aop-ca.key private and backed up: you need it to create a new client certificate later.
Step 6: Turn on Authenticated Origin Pulls in Cloudflare
For each Cloudflare zone (each domain) hosted on the server, upload the client certificate and turn the feature on. Uploading your own certificate is done through Cloudflare's API. Create an API token in Cloudflare with Zone > SSL and Certificates > Edit permission for those zones, and find each zone's Zone ID on its Overview page. The same certificate can be used for all of your zones.
Upload the certificate (needs the jq tool to package the files):
TOKEN=your_api_token
ZONE=your_zone_id
jq -n --rawfile c /etc/pki/cf-aop/aop-client.pem --rawfile k /etc/pki/cf-aop/aop-client.key '{certificate:$c, private_key:$k}' | \
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/origin_tls_client_auth" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data @-
Check that the certificate's status is active (this can take a few minutes):
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE/origin_tls_client_auth" -H "Authorization: Bearer $TOKEN"
Then turn on Authenticated Origin Pulls for the zone, in the dashboard under SSL/TLS > Origin Server > Authenticated Origin Pulls, or with the API:
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE/origin_tls_client_auth/settings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data '{"enabled":true}'
Repeat for each zone. Cloudflare's documentation for Authenticated Origin Pulls has more detail on these steps.
Step 7: Require the certificate in Apache
For each website domain proxied by Cloudflare, create this file (replace CPUSER with the cPanel username and DOMAIN with the 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
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.
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 (blocked by the firewall):
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/
- On the server itself, an HTTPS connection without the certificate should be refused during the TLS handshake (this checks Part 2, since the firewall doesn't block the server's own connections):
curl -k --max-time 10 --resolve example.com:443:127.0.0.1 https://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
# Copyright (c) 2026 Acenet, Inc.
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.
Renew the origin certificate
The client certificate from step 5 is valid for 825 days. Before it expires, create a new one from the same CA (the last two openssl commands in step 5) and upload it to each zone as in step 6. Apache keeps working without changes, because it trusts the CA, not the individual certificate. Add a calendar reminder.
Undoing the changes
- Part 2: delete the cf-aop.conf files, run /scripts/rebuildhttpdconf && /scripts/restartsrv_httpd, and then turn Authenticated Origin Pulls off in Cloudflare.
- Part 1: restore the backups from step 1 and restart the firewall, turn Force HTTPS Redirect back off if you don't want it, and remove /etc/cron.d/cloudflare-ips-update:
cp -p /etc/csf/csf.conf.bak /etc/csf/csf.conf cp -p /etc/csf/csf.allow.bak /etc/csf/csf.allow csf -r
If you have any questions, please open a support ticket from your client area.