Skip to content
StealthRDP
VPS Management

Allow Only Cloudflare IPs on Nginx, Apache and IIS

How to allow only Cloudflare IPs on ports 80 and 443 with UFW, nginx, Apache and IIS, keep the list current and stop direct hits on your origin IP.

Published

9 min readBy StealthRDP Team

Cloudflare's proxy hides your server's IP address and adds DDoS protection, but only for traffic that actually passes through Cloudflare. [1] Anyone who learns the real IP can connect to the server directly and skip all of it. This guide shows how to allow only Cloudflare IPs on ports 80 and 443, with UFW and nginx or Apache on Ubuntu 24.04 and with Windows Defender Firewall and IIS on Windows Server 2022 or 2025, and how to keep Cloudflare's list current.

Why anyone with your server's IP can skip Cloudflare

Requests sent straight to your server's IP never pass through Cloudflare, so none of its protection applies to them. A proxied DNS record hides the origin IP, and Cloudflare asks you to allow its IP addresses at the origin so its own requests get through. [1] Its network-level advice goes further: explicitly block all traffic that does not come from Cloudflare or from partners you trust. [1]

Limit only TCP 80 and 443 to Cloudflare. Keep SSH on Linux and Remote Desktop on Windows limited to your own IP address or a VPN: our SSH hardening checklist and the guide to a WireGuard private network cover both.

Cloudflare rates an IP allowlist as moderately secure, and Authenticated Origin Pulls and Cloudflare Tunnel as very secure. [1] So this guide stacks the checks:

Layer What it checks Tools
1. Firewall Source is a Cloudflare range UFW, Windows Defender Firewall
2. Web server Real visitor IP, unknown host names nginx, Apache, IIS
3. Authenticated Origin Pulls Cloudflare's client certificate nginx or Apache
Alternative: Cloudflare Tunnel No open web ports cloudflared

A host firewall cannot stop a flood big enough to fill the server's connection. StealthRDP EU plans include network-level DDoS protection; USA plans do not. Our VPS DDoS protection checklist covers that side.

Where Cloudflare publishes its IP ranges

Cloudflare's IP Ranges page at www.cloudflare.com/ips is meant to be the definitive list. It links plain-text copies at www.cloudflare.com/ips-v4 and www.cloudflare.com/ips-v6, and the same list is in the Cloudflare API. [2]

The list does change. Its update history shows 199.27.128.0/21 removed in 2017, 104.16.0.0/12 replaced by 104.16.0.0/13 and 104.24.0.0/14 in 2021, and 2405:b500::/32 removed in 2022 and added back in 2023. [2] If you miss a range, Cloudflare's requests from it are blocked [1], so a list pasted in once will go stale.

How to allow only Cloudflare IPs in UFW

To allow only Cloudflare IPs in UFW, add rules for ports 80 and 443 from each Cloudflare range, then delete the rules that open those ports to everyone. UFW is Ubuntu's default firewall tool and starts out disabled; it can open a port to a single host or network, as in sudo ufw allow proto tcp from 192.168.0.2 to any port 22, and it handles IPv4 and IPv6. [3]

Allow SSH from your own IP before you enable UFW or delete any rule. If you lock yourself out, only your provider's console will get you back in.

1. Keep SSH open for your own IP

Replace 198.51.100.7 with your own public IP
sudo ufw allow proto tcp from 198.51.100.7 to any port 22
sudo ufw enable
sudo ufw status verbose

2. Add the refresh script

The script downloads both lists and stops if either looks wrong. It adds rules for new ranges before deleting rules for dropped ones, so the site keeps working during a run; ufw delete followed by the rule removes a rule. [3] It also writes the ranges for nginx and Apache.

/usr/local/sbin/cloudflare-ips
#!/usr/bin/env bash
# Rebuild Cloudflare's IP ranges for UFW, nginx and Apache.
set -euo pipefail
export LC_ALL=C PATH=/usr/sbin:/usr/bin:/sbin:/bin

STATE=/etc/cloudflare-ips.txt
NEW=$(mktemp)
trap 'rm -f "$NEW"' EXIT

v4=$(curl -fsS --max-time 30 https://www.cloudflare.com/ips-v4)
v6=$(curl -fsS --max-time 30 https://www.cloudflare.com/ips-v6)
printf '%s\n%s\n' "$v4" "$v6" | grep -E '^[0-9a-fA-F:.]+/[0-9]+$' | sort -u > "$NEW"

# Stop before touching anything if a list is missing or cut short.
if ! grep -q '\.' "$NEW" || ! grep -q ':' "$NEW" || [ "$(wc -l < "$NEW")" -lt 10 ]; then
  echo "Cloudflare lists look wrong; nothing changed" >&2
  exit 1
fi

touch "$STATE"
# Add new ranges first, then remove the ones Cloudflare dropped.
comm -13 "$STATE" "$NEW" | while read -r net; do
  ufw allow proto tcp from "$net" to any port 80
  ufw allow proto tcp from "$net" to any port 443
done
comm -23 "$STATE" "$NEW" | while read -r net; do
  ufw delete allow proto tcp from "$net" to any port 80 || true
  ufw delete allow proto tcp from "$net" to any port 443 || true
done
cp "$NEW" "$STATE"

if command -v nginx > /dev/null; then
  { sed 's/.*/set_real_ip_from &;/' "$NEW"; echo 'real_ip_header CF-Connecting-IP;'; } \
    > /etc/nginx/conf.d/cloudflare-realip.conf
  nginx -t && systemctl reload nginx
fi

if command -v apache2ctl > /dev/null; then
  cp "$NEW" /etc/apache2/cloudflare-ips.lst
  apache2ctl configtest && systemctl reload apache2
fi

3. Schedule it and close the open web rules

Run the script once, then let cron run it daily:

Install, run once and schedule daily
sudo chmod 755 /usr/local/sbin/cloudflare-ips
sudo /usr/local/sbin/cloudflare-ips
echo '17 4 * * * root /usr/local/sbin/cloudflare-ips' | sudo tee /etc/cron.d/cloudflare-ips

Delete the open web rules only after the script has added Cloudflare's ranges. Otherwise the site goes offline for everyone.

Then list the rules by number and delete every rule that still allows 80 or 443 from anywhere, application profiles included, using the same form you used to add it. [3]

Remove the rules that open 80 and 443 to everyone
sudo ufw status numbered
# For example, if you once ran "ufw allow 80" and "ufw allow 443":
sudo ufw delete allow 80
sudo ufw delete allow 443
sudo ufw status numbered

Docker-published ports skip UFW

If your web server runs in Docker with published ports, these UFW rules do not protect it.

Docker diverts a published port's traffic in the nat table, before it reaches the INPUT chain UFW uses, so UFW's rules are ignored for that port. [4] Docker also advises against editing its rules, and turning off its iptables handling is likely to break container networking. [4] Keep the web server that faces Cloudflare on the host, or filter in a firewall in front of the server.

nginx and Apache: restore visitor IPs and refuse the bare IP

Both web servers can swap Cloudflare's address for the visitor's, which Cloudflare sends in the CF-Connecting-IP header. [5] You need this because, with the firewall in place, every request comes from a Cloudflare address. Trust the header only from Cloudflare's ranges; Cloudflare warns it could otherwise be spoofed. [5]

nginx

Cloudflare's nginx setup uses the realip module: a set_real_ip_from line per Cloudflare range plus real_ip_header CF-Connecting-IP, with the ranges kept up to date. [5] The refresh script writes this file:

/etc/nginx/conf.d/cloudflare-realip.conf (written by the script)
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
# ...one line for every Cloudflare range...
set_real_ip_from 2c0f:f248::/32;
real_ip_header CF-Connecting-IP;

Next, refuse requests for names you don't host, such as a request to the bare IP: Cloudflare advises accepting only the Host header values you expect. [1] A default server does that in nginx. deny all refuses every request that reaches it [6], and ssl_reject_handshake on rejects TLS handshakes in its server block; nginx's own example pairs it with default_server, so handshakes for any other name fail. The directive needs nginx 1.19.4 or later. [7]

/etc/nginx/sites-available/catch-all
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    deny all;
}

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    ssl_reject_handshake on;
}

Remove default_server from any other server block on these ports, enable the file and reload nginx.

nginx could also hold Cloudflare's list as allow lines ending in deny all, since rules are checked in order until one matches. [6] Use that only on a server that does not restore visitor IPs, and log $http_cf_connecting_ip there instead. [5] A restored address is the visitor's, so a Cloudflare-only allow list would refuse real visitors, as Apache's documentation spells out for its own module. [8]

Apache 2.4

Cloudflare's Apache steps use mod_remoteip: enable the module, set RemoteIPHeader CF-Connecting-IP, list Cloudflare's ranges as trusted proxies in /etc/apache2/conf-available/remoteip.conf, enable that file, and change %h to %a in the combined LogFormat in /etc/apache2/apache2.conf. [5] RemoteIPTrustedProxyList reads the ranges from a file at startup, which suits the script. [8]

/etc/apache2/conf-available/remoteip.conf
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxyList /etc/apache2/cloudflare-ips.lst
Enable mod_remoteip and the configuration
sudo a2enmod remoteip
sudo a2enconf remoteip
sudo apache2ctl configtest
sudo systemctl restart apache2

Two details matter. Without trusted proxies, mod_remoteip trusts the header from any host, so anyone could fake their address. [8] And after the swap, Require ip and the %a log field see the visitor's address, while %{c}a and CONN_REMOTE_ADDR keep the Cloudflare address that connected. [8] So keep Cloudflare's ranges in the trusted list and the firewall, and use Require ip only for rules about visitors, such as an admin page limited to your IP.

IIS on Windows Server: limit ports 80 and 443 to Cloudflare

On a Windows VPS with Windows Server 2022 or 2025, Windows Defender Firewall does the same job from PowerShell. New-NetFirewallRule takes IPv4 and IPv6 subnets in CIDR form for -RemoteAddress, so one rule holds every range, and Set-NetFirewallRule changes an existing rule. [9]

Leave the Remote Desktop rules alone and keep RDP limited to your own IP. Disabling the wrong rule can lock you out of the server.

Allow 80 and 443 from Cloudflare only (run as Administrator, then daily in Task Scheduler)
$ErrorActionPreference = 'Stop'
$ranges = foreach ($list in 'ips-v4', 'ips-v6') {
    (Invoke-RestMethod "https://www.cloudflare.com/$list") -split "`n" |
        ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^[0-9a-fA-F:.]+/\d+$' }
}
$v6 = @($ranges | Where-Object { $_ -match ':' })
if ($ranges.Count -lt 10 -or $v6.Count -eq 0 -or $v6.Count -eq $ranges.Count) {
    throw 'Cloudflare lists look wrong; nothing changed'
}

$name = 'Cloudflare HTTP and HTTPS'
if (Get-NetFirewallRule -DisplayName $name -ErrorAction SilentlyContinue) {
    Set-NetFirewallRule -DisplayName $name -RemoteAddress $ranges
} else {
    New-NetFirewallRule -DisplayName $name -Direction Inbound -Protocol TCP `
        -LocalPort 80,443 -RemoteAddress $ranges -Action Allow
}

An allow rule lets in every packet that matches it [9], so any other enabled rule that allows 80 or 443 from anywhere keeps them open to everyone. List those rules, check each one and disable the ones that aren't yours:

Find other inbound rules that still open 80 or 443
Get-NetFirewallRule -Direction Inbound -Action Allow -Enabled True |
    Where-Object { $_.DisplayName -ne 'Cloudflare HTTP and HTTPS' -and
        (($_ | Get-NetFirewallPortFilter).LocalPort -match '^(80|443)$') } |
    Select-Object Name, DisplayName
# After checking a rule: Set-NetFirewallRule -Name '<Name>' -Enabled False

For IIS 8.5 to 10, Cloudflare's steps log the real visitor by adding CF-Connecting-IP as a custom field: in IIS Manager, open Logging for the site, select the W3C format, choose Select Fields and add the header. [5] That changes the log only; your application still sees Cloudflare's address. [5]

Authenticated Origin Pulls: the strongest origin check

Authenticated Origin Pulls (AOP) adds mutual TLS: Cloudflare presents a client certificate when it connects to your origin, and the web server checks it. It needs the SSL/TLS encryption mode Full or higher. [10] The global setup uses a Cloudflare certificate shared by all Cloudflare accounts, so it only shows that a request came from Cloudflare's network; for a check tied to your account, upload your own certificate with zone-level or per-hostname AOP. [10]

Download Cloudflare's origin pull certificate (.PEM) from the setup guide and copy it to the server; it is not the Cloudflare Origin CA certificate. [10] Point the web server at it without enforcing anything yet; in nginx that means ssl_verify_client optional, which asks for a client certificate and verifies it if one is sent. [7] Then switch AOP on in Cloudflare: Origin Server page, Authenticated Origin Pulls tab, Global section. [10]

Enforce the check only after AOP is on in Cloudflare and the site still works. If the origin demands a certificate that Cloudflare does not send, every visitor gets an error.

When the site still works, enforce it: ssl_verify_client on in nginx, SSLVerifyClient require in Apache. [10]

nginx: in your site's HTTPS server block
ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
ssl_verify_client optional;   # change to "on" to enforce
Apache: in your site's SSL virtual host
SSLCACertificateFile /etc/apache2/certs/origin-pull-ca.pem
# add this line to enforce:
SSLVerifyClient require

Cloudflare Tunnel: no inbound web ports at all

Cloudflare Tunnel closes the web ports instead of filtering them. The cloudflared daemon on your server makes outbound-only connections to Cloudflare, so the site needs no publicly routable IP address. [1] With the tunnel up, ports 80 and 443 can stay closed. Cloudflare rates Tunnel as very secure and offers it to all customers; the cost is one more service to run. [1]

How origin IPs leak, and how to test the lockdown

Origin IPs usually leak through DNS-only records, mail on the same server or old DNS history, as Cloudflare's origin-protection guide explains. [1]

  • DNS-only records that contain the origin IP, SPF and TXT records included.
  • Mail on the same server: mail to a non-existent address bounces back and shows the mail server's IP.
  • DNS history: old records are kept and show the IP you used before Cloudflare.

Cloudflare's advice is to rotate the origin IP once you are onboarded. [1] Do it after the lockdown, so the new address has never been public. Every StealthRDP server, Linux VPS or Windows, has a dedicated IPv4 address, and an IP change costs €5 per change, requested through support.

Then test from your own computer, which the firewall keeps off ports 80 and 443:

From your own computer, not from the server
# Straight to the origin IP: both should hang, then fail
curl http://203.0.113.10/
curl https://203.0.113.10/

# Through Cloudflare: should return your page
curl https://www.example.com/

A timeout is what you want. A 403 page, or any TLS or certificate error, means the request reached the web server, so the firewall is still open.

Once AOP is enforced, a request that does not come through Cloudflare should also fail the certificate check. [10] From your computer the firewall stops such a request before it gets that far, so run this test on the server itself:

On the server: connect to your site without Cloudflare's certificate
curl -k --resolve www.example.com:443:127.0.0.1 https://www.example.com/

nginx should answer with error 400 because no client certificate was sent, and Apache should refuse the TLS handshake. If visitors see Cloudflare error pages afterwards, check that every range is in place; our 502 Bad Gateway guide covers other origin errors behind Cloudflare.

FAQ

Where can I find Cloudflare's IP ranges?

Cloudflare publishes them at www.cloudflare.com/ips, with plain-text lists at www.cloudflare.com/ips-v4 and www.cloudflare.com/ips-v6 and the same data in its API. The list changes from time to time, so refresh your copy automatically. [2]

Can someone bypass Cloudflare with my server's IP?

Yes, if the server accepts web traffic from anyone. DNS-only records, mail bounces and old DNS records can reveal the IP, so Cloudflare advises blocking traffic that does not come from its addresses. [1]

How do I see the real visitor IP behind Cloudflare?

Cloudflare sends it in the CF-Connecting-IP header. nginx's realip module and Apache's mod_remoteip can use it, as long as you trust the header only from Cloudflare's ranges. [5]

Does UFW block Docker ports?

Not for ports that a container publishes. Docker diverts that traffic in the nat table before it reaches the chains UFW uses, so UFW's rules are ignored for those ports. [4]

Do I need open ports for Cloudflare Tunnel?

No inbound web ports are needed. The cloudflared daemon makes outbound-only connections to Cloudflare, so the server needs no publicly routable IP address for the site. [1]

Sources & references
  1. Protect your origin server — Cloudflare (accessed 2026-11-03)
  2. IP Ranges — Cloudflare (accessed 2026-11-03)
  3. Firewall — Ubuntu Server documentation (accessed 2026-11-03)
  4. Packet filtering and firewalls — Docker Docs (accessed 2026-11-03)
  5. Restoring original visitor IPs — Cloudflare (accessed 2026-11-03)
  6. Module ngx_http_access_module — nginx (accessed 2026-11-03)
  7. Module ngx_http_ssl_module — nginx (accessed 2026-11-03)
  8. Apache Module mod_remoteip — Apache HTTP Server Project (accessed 2026-11-03)
  9. New-NetFirewallRule — Microsoft Learn (accessed 2026-11-03)
  10. Global authenticated origin pulls — Cloudflare (accessed 2026-11-03)

Continue learning