Restrict Web Traffic to Cloudflare on a cPanel Server

From Acenet Knowledgebase
Revision as of 15:44, 26 October 2026 by Docs admin (talk | contribs) (Accuracy review: leaf client certificate, dashboard upload, addon-domain include folders, on-server test IP, mod_remoteip check; script uses sendmail and respects CSF IPv6 setting)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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. Part 1 is required; Part 2 is optional but recommended:

  1. Firewall (IP restriction): only Cloudflare's IP addresses can connect to your server's HTTPS port (443). This blocks direct connections from everyone else.
  2. 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

  1. Open /etc/csf/csf.conf.
  2. Remove 443 from the TCP_IN line, and from the TCP6_IN line if your server uses IPv6. Leave 80 in both.
  3. 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 (optional, recommended): Accept only your own Cloudflare account (Authenticated Origin Pulls)

Part 1 already blocks everyone who connects to your server directly. Part 2 is optional, but it closes a gap that a firewall can't.

What risk does this protect against?

Cloudflare's network is shared by all of its customers, and anyone can create a free Cloudflare account. After Part 1, your server accepts HTTPS connections from any Cloudflare IP address, and it can't tell whether a connection is coming through your Cloudflare account or someone else's. An attacker who finds your server's IP address can:

  • Add your server's IP to their own Cloudflare account (or use a Cloudflare Worker) and send requests to your websites through Cloudflare. Their traffic passes your firewall because it comes from Cloudflare's IP addresses.
  • Skip your Cloudflare protections. Cloudflare's firewall rules, rate limits, bot protection and access rules are set per Cloudflare account. Requests sent through the attacker's account follow the attacker's settings, not yours, so your rules don't apply to them. This is the same problem you set out to fix by blocking direct access, just routed through Cloudflare.
  • Probe, attack and overload your websites (for example login attacks, vulnerability scans or exploit attempts against your website software) without being stopped by the protections you've configured.

With Authenticated Origin Pulls, Cloudflare presents a private certificate on every connection from your Cloudflare zones, and Apache refuses any HTTPS connection that doesn't present it. Connections through someone else's Cloudflare account don't have your certificate, so they're refused, and only traffic that went through your own Cloudflare settings reaches your websites.

If your websites handle logins, payments or other sensitive data, or you rely on Cloudflare's firewall rules to protect them, we recommend doing Part 2. It needs access to your Cloudflare account (step 6).

How it works

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. The extensions file makes the client certificate a leaf (end-entity) certificate for client authentication; Cloudflare rejects a certificate that isn't a leaf.

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"
printf 'basicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=clientAuth\n' > aop-client.ext
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 -extfile aop-client.ext -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. The same certificate can be used for all of your zones.

In the Cloudflare dashboard: for each zone, go to SSL/TLS > Origin Server, open the Authenticated Origin Pulls tab, and in the Zone-level section select Upload certificate. Paste the contents of aop-client.pem and aop-client.key, continue, and then switch Zone-level Authenticated Origin Pulls On.

Or with Cloudflare's API, which is quicker for many zones. Create an API token with Zone > SSL and Certificates > Edit permission for those zones, and find each zone's Zone ID on its Overview page. Upload the certificate (this uses the jq tool to package the files; install it with dnf install jq if needed):

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 zone-level Authenticated Origin Pulls for the zone:

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

Each website has its own HTTPS section (virtual host) in Apache's configuration, and cPanel loads extra settings for it from a folder named after the cPanel username and the virtual host's name. For addon domains, the virtual host is named after the addon's subdomain (for example addon.maindomain.com), not the addon domain itself, so look up the exact folders rather than guessing. This lists every HTTPS include folder for an account (replace CPUSER):

grep -o '/etc/apache2/conf.d/userdata/ssl/2_4/CPUSER/[^/]*/' /etc/apache2/conf/httpd.conf | sort -u

For each website proxied by Cloudflare, create the folder from that list and put this file in it as cf-aop.conf (cPanel keeps files in these folders when it rebuilds the Apache configuration):

mkdir -p /etc/apache2/conf.d/userdata/ssl/2_4/CPUSER/VHOSTNAME
cat > /etc/apache2/conf.d/userdata/ssl/2_4/CPUSER/VHOSTNAME/cf-aop.conf <<'CONF'
SSLVerifyClient require
SSLVerifyDepth 1
SSLCACertificateFile /etc/pki/cf-aop/aop-ca.pem
CONF

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.

Check that the module is installed (the command should print remoteip_module). If it doesn't, install mod_remoteip in WHM under Software > EasyApache 4 (or run dnf install ea-apache24-mod_remoteip):

httpd -M | grep remoteip

Then 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/
curl -I --resolve example.com:80:203.0.113.10 http://example.com/
  • If you did Part 2: on the server itself, an HTTPS connection without the certificate should be refused during the TLS handshake. The firewall doesn't block the server's own connections, so this tests Part 2 on its own. Use the server's IP address, not 127.0.0.1, because cPanel's website configurations are tied to the server's IP addresses:
curl -k --max-time 10 --resolve example.com:443:203.0.113.10 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. The IPv6 ranges are added to the firewall only if IPv6 is turned on in csf.conf. 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.
# Use at your own risk.
set -u
PORTS="443"                                       # ports Cloudflare may reach, e.g. "80 443"
CSF_CONF=/etc/csf/csf.conf
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
LOCK=/var/run/cloudflare-ips-update.lock
MAILTO=root
APACHECTL=/usr/sbin/apachectl
RESTART_HTTPD=/scripts/restartsrv_httpd
BEGIN="# BEGIN cloudflare-ips-update (managed - do not edit)"
END="# END cloudflare-ips-update"

log(){ echo "$(date '+%F %T') $*" >> "$LOG"; }
alert(){ log "ALERT: $1"; printf 'Subject: cloudflare-ips-update on %s: %s\n\n%s\n' "$(hostname)" "$2" "$1" | /usr/sbin/sendmail "$MAILTO" 2>/dev/null; }

exec 9>"$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"

# CSF only gets the IPv6 ranges if IPv6 is enabled in csf.conf; Apache always gets both
if grep -qE '^IPV6 *= *"1"' "$CSF_CONF"; then cp "$TMP/all" "$TMP/csf"; v6on=1; else cp "$TMP/v4.ok" "$TMP/csf"; v6on=0; fi
{ echo "ports=$PORTS ipv6=$v6on"; cat "$TMP/all"; } > "$TMP/state"

if [ -f "$STATE" ] && cmp -s "$TMP/state" "$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/csf"
  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: put the new RemoteIPTrustedProxy lines where the old ones were
#    (or before the last </IfModule>, or at the end), keep every other line
if [ -f "$APACHE_CF" ]; then
  awk -v list="$TMP/all" '
    function emit(ind,  c) { while ((getline c < list) > 0) print ind "RemoteIPTrustedProxy " c; close(list); done=1 }
    { line[NR]=$0 }
    /^[[:space:]]*<\/IfModule>/ { lastclose=NR }
    END {
      for (i=1; i<=NR; i++) {
        if (line[i] ~ /^[[:space:]]*RemoteIPTrustedProxy[[:space:]]/) {
          if (!done) { match(line[i], /^[[:space:]]*/); emit(substr(line[i], 1, RLENGTH)) }
          continue
        }
        if (!done && !hasold && i==lastclose) emit("    ")
        print line[i]
      }
      if (!done) emit("")
    }
    BEGIN { while ((getline l < ARGV[1]) > 0) if (l ~ /^[[:space:]]*RemoteIPTrustedProxy[[:space:]]/) hasold=1; close(ARGV[1]) }
  ' "$TMP/apache_cf.bak" > "$TMP/apache_cf.new"
  cp "$TMP/apache_cf.new" "$APACHE_CF"
  if ! "$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" ] && "$RESTART_HTTPD" >/dev/null 2>&1

cp "$TMP/state" "$STATE"
log "updated: $n4 v4, $n6 v6 ranges (IPv6 in CSF: $v6on), 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 three commands in step 5, run in /etc/pki/cf-aop) 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.