Skip to content
rodolfo.gg
Go back

How to restrict IP address access to a Cloudflare proxy host.

CC BY-NC-ND 4.0
Rodolfo González González

How to restrict IP address access to a Cloudflare proxy host.

There are many web applications that need access restricted to only certain IP addresses. This can normally be achieved at the web server level:

However, using Cloudflare as a reverse proxy is becoming increasingly popular. A reverse proxy is a service that receives requests from clients and forwards them to one or more origin servers located behind it. Clients do not communicate directly with those servers, but with the reverse proxy, which acts as an intermediary between both ends, as shown in the following figure:

Reverse proxy

In our scenario, the origin server must accept connections only from Cloudflare’s IP ranges (which can change over time) and enforce the restriction using the real client IP.


Table of Contents

Table of Contents

Cloudflare

In the Cloudflare console

The first part of the solution is handled in Cloudflare.

In the Cloudflare dashboard, go to Security → WAF → Custom rules and create a rule:

  1. Enter an easy-to-identify name:

Name: Office only

  1. Create an “expression” or rule that tells Cloudflare’s WAF what to do. This can be the most complex part.

Expression (Edit expression):

A simple rule could be

(ip.src ne a.b.c.d)

which means “the source IP is not a.b.c.d”. This blocks all traffic not coming from your office IP before it reaches your server. If you later need to add another IP, the expression would look something like:

(ip.src ne a.b.c.d and ip.src ne e.f.g.h).

To apply a rule to specific hosts, you can do something like:

(ip.src ne a.b.c.d and http.host in {"app.example.com" "admin.example.com"})

This way, www.example.com or other subdomains pointing to unrestricted servers remain unaffected.

  1. Select the desired action, in this case Block:

Action: Block

This causes Cloudflare to block requests coming from IP addresses that do not match the rule.

On the web server

As mentioned above, Cloudflare proxy IP addresses can change periodically. The following script updates a file with those addresses:

/usr/local/sbin/update-cloudflare-proxies.sh
#!/usr/bin/env bash
set -euo pipefail
RAW="/var/lib/cloudflare/cloudflare-trusted-proxies.lst"
TMP="$(mktemp)"
cleanup() { rm -f "$TMP"; }
trap cleanup EXIT
mkdir -p "$(dirname "$RAW")"
{
echo "# Cloudflare trusted proxies"
echo "# Generated on $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo
curl -fsSL https://www.cloudflare.com/ips-v4
echo
curl -fsSL https://www.cloudflare.com/ips-v6
} > "$TMP"
LINES=$(grep -cE '^[0-9a-f:.\/]+$' "$TMP" || true)
if [ "$LINES" -lt 5 ]; then
echo "ERROR: only $LINES ranges obtained. Aborting." >&2
exit 1
fi
if cmp -s "$TMP" "$RAW" 2>/dev/null; then
echo "No changes."
exit 0
fi
mv "$TMP" "$RAW"
chmod 644 "$RAW"
echo "List updated ($LINES ranges)."

This script can be run with cron or systemd. Since I’m not a fan of systemd, here is the procedure for cron:

Terminal window
sudo chmod +x /usr/local/sbin/update-cloudflare-proxies.sh
Terminal window
sudo crontab -e
# Update Cloudflare proxies — Monday 4:00 AM
0 4 * * 1 /usr/local/sbin/update-cloudflare-proxies.sh >> /var/log/cloudflare-proxies.log 2>&1

First run of the script:

Terminal window
sudo /usr/local/sbin/update-cloudflare-proxies.sh

Now, you need to tell the origin web server the following:

  1. The list of Cloudflare proxy IP addresses.
  2. The allowed IP address.
  3. The access restriction for all IP addresses except the allowed one.

Apache

The remoteip module is required to obtain the real remote IP address.

Terminal window
sudo a2enmod remoteip
sudo systemctl restart apache2

Apache reads the list directly, without conversion:

/etc/apache2/conf-available/cloudflare-remoteip.conf
<IfModule mod_remoteip.c>
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxyList /var/lib/cloudflare/cloudflare-trusted-proxies.lst
</IfModule>

Using RemoteIPHeader CF-Connecting-IP is vital, as it tells Apache that the real client address comes from Cloudflare in that header.

Enable that configuration, either by creating the symlink in conf-enabled, or with:

Terminal window
sudo a2enmod remoteip
sudo systemctl restart apache2

Then, apply the restriction for the source IP:

/etc/apache2/sites-available/example.com.conf
<VirtualHost *:443>
ServerName www.example.com
# ... your SSL config, DocumentRoot, etc. ...
# Allow only your office IP
<Location "/">
Require ip a.b.c.d 2001:db8::1
</Location>
</VirtualHost>
Terminal window
sudo apache2ctl configtest
sudo systemctl reload apache2

nginx

nginx has no equivalent to TrustedProxyList, so you need to generate a snippet from the list:

/usr/local/sbin/gen-nginx-cloudflare.sh
#!/usr/bin/env bash
set -euo pipefail
SRC="/var/lib/cloudflare/cloudflare-trusted-proxies.lst"
OUT="/etc/nginx/snippets/cloudflare-trusted-proxies.conf"
{
echo "# Generated from $SRC — do not edit"
grep -E '^[0-9a-f:.\/]+$' "$SRC" | while IFS= read -r cidr; do
echo "set_real_ip_from $cidr;"
done
echo "real_ip_header CF-Connecting-IP;"
} > "$OUT"
nginx -t 2>/dev/null && systemctl reload nginx
/etc/nginx/sites-available/app.example.com
server {
listen 443 ssl;
server_name app.example.com;
include snippets/cloudflare-trusted-proxies.conf;
allow a.b.c.d;
allow 2001:db8::1;
deny all;
# ...
}

lighttpd

Same situation, requires a generated snippet:

/usr/local/sbin/gen-lighttpd-cloudflare.sh
#!/usr/bin/env bash
set -euo pipefail
SRC="/var/lib/cloudflare/cloudflare-trusted-proxies.lst"
OUT="/etc/lighttpd/conf-available/90-cloudflare-trusted-proxies.conf"
{
echo "# Generated from $SRC — do not edit"
echo 'server.modules += ("mod_extforward")'
echo 'extforward.headers = ("CF-Connecting-IP")'
printf 'extforward.forwarder = ('
FIRST=1
grep -E '^[0-9a-f:.\/]+$' "$SRC" | while IFS= read -r cidr; do
[ "$FIRST" -eq 1 ] && FIRST=0 || printf ','
printf '\n "%s" => "trust"' "$cidr"
done
echo
echo ')'
} > "$OUT"
lighttpd -t -f /etc/lighttpd/lighttpd.conf 2>/dev/null && systemctl reload lighttpd
/etc/lighttpd/lighttpd.conf
include "conf-available/90-cloudflare-trusted-proxies.conf"
# IP restriction on the vhost
$HTTP["host"] == "app.example.com" {
$HTTP["remoteip"] !~ "^(a\.b\.c\.d|2001:db8::1)$" {
url.access-deny = ("")
}
}

On the firewall

As an additional layer of defense, configure ufw so that port 443 (and 80) only accept traffic from Cloudflare’s ranges, rejecting direct connections from any other IP:

#!/usr/bin/env bash
set -euo pipefail
SRC="/var/lib/cloudflare/cloudflare-trusted-proxies.lst"
if [ ! -f "$SRC" ]; then
echo "ERROR: $SRC does not exist. Run update-cloudflare-proxies.sh first." >&2
exit 1
fi
CIDRS=$(grep -E '^[0-9a-f:.\/]+$' "$SRC")
if [ -z "$CIDRS" ]; then
echo "ERROR: no CIDRs found in $SRC." >&2
exit 1
fi
# Remove previous Cloudflare rules on ports 80,443
ufw status numbered | grep -E '80,443' | grep -oP '^\[\s*\K[0-9]+' | sort -rn | while read -r num; do
yes | ufw delete "$num"
done
# Add updated rules
while IFS= read -r cidr; do
ufw allow from "$cidr" to any port 80,443 proto tcp
done <<< "$CIDRS"
ufw reload
echo "ufw updated with $(echo "$CIDRS" | wc -l) ranges."

This prevents anyone who discovers your server’s IP from connecting directly, bypassing Cloudflare.


Verification

From your office IP, access https://www.example.com — it should work normally. From another network (for example your mobile phone on mobile data), try to access it: you should receive a Cloudflare block (error 1020).

Check the Apache logs to confirm that real IPs are recorded correctly. In the case of Apache:

Terminal window
tail -f /var/log/apache2/access.log

You should see a.b.c.d and not a Cloudflare IP.


Defense model summary

LayerWhat it doesWhat it blocks
Cloudflare WAFFilters by visitor IPUnauthorized IP traffic before it reaches the server
Apache + mod_remoteipFilters by restored real IPUnauthorized traffic that manages to pass through Cloudflare
ufwFilters by source IP at network levelDirect connections to the server that bypass Cloudflare

The three layers together give you defense in depth: if one fails, the others hold the restriction.


Bonus

How do I get my “outgoing” IP address?

You can use a site like whatismyip to obtain your public outgoing IP address.

Scripts

You can find the scripts on my GitHub, under the GPL 3.0 license.


Share this post on:

Previous Post
How to Install Argilla Without Losing Your Mind.
Next Post
How to use Go modules from private GitHub repositories.