Skip to content

Instantly share code, notes, and snippets.

@irazasyed
Last active August 2, 2026 17:21
Show Gist options
  • Select an option

  • Save irazasyed/553a9f579e200690f2c295da43a08002 to your computer and use it in GitHub Desktop.

Select an option

Save irazasyed/553a9f579e200690f2c295da43a08002 to your computer and use it in GitHub Desktop.
Restrict the origin's web ports to Cloudflare edge IPs.
[Unit]
Description=Restrict origin web ports (80/443) to Cloudflare edge IPs
Documentation=file:/usr/local/sbin/cf-origin-lock.sh
# The script fetches Cloudflare's published ranges, so it needs real
# connectivity rather than just a configured interface. It falls back to the
# cached list in /var/lib/cf-origin-lock if the fetch fails.
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/cf-origin-lock.sh
RemainAfterExit=yes
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
#!/usr/bin/env bash
#
# Restrict the origin's web ports to Cloudflare edge IPs.
#
# The origin answers on its bare IP, so anyone who knows it can reach the site
# without passing through Cloudflare at all -- WAF, rate limiting and custom
# rules included. That exposure is also what makes useIpInProxyHeader unsafe on
# its own: trusting X-Forwarded-For while the origin is openly reachable lets
# anyone forge a client IP, including one of QUIC.cloud's.
#
# Only 80 and 443 are touched. SSH (22) and CyberPanel (8090) are deliberately
# left alone so a mistake here cannot lock anyone out of the box. Loopback is
# accepted first so the wp-cron job on 127.0.0.1 keeps working, and
# ESTABLISHED/RELATED is accepted so in-flight connections survive re-runs.
#
# Run at boot and weekly by cf-origin-lock.timer: Cloudflare adds ranges from
# time to time, and a stale list would silently drop edge traffic.
set -euo pipefail
CACHE_DIR=/var/lib/cf-origin-lock
mkdir -p "$CACHE_DIR"
# Fetch the current ranges, falling back to the last known good copy.
#
# A failed fetch must never produce a partial ruleset: the chain ends in DROP,
# so an empty list would blackhole every web request. Preferring a stale cache
# over an empty fetch keeps the failure mode "slightly out of date" rather than
# "site offline".
fetch_list() {
local url="$1" cache="$2" min="$3" fresh
fresh=$(curl -fsS --max-time 20 "$url" 2>/dev/null || true)
if [ "$(printf '%s\n' "$fresh" | grep -c .)" -ge "$min" ]; then
printf '%s\n' "$fresh" > "$cache"
printf '%s\n' "$fresh"
return 0
fi
if [ -s "$cache" ] && [ "$(grep -c . "$cache")" -ge "$min" ]; then
echo "WARN: fetch of $url failed or was short; using cached list" >&2
cat "$cache"
return 0
fi
echo "ERROR: no usable list for $url and no cache; refusing to apply" >&2
return 1
}
CF4=$(fetch_list https://www.cloudflare.com/ips-v4 "$CACHE_DIR/ips-v4.txt" 10)
CF6=$(fetch_list https://www.cloudflare.com/ips-v6 "$CACHE_DIR/ips-v6.txt" 3)
apply() {
local cmd="$1" list="$2"
# Rebuild the allowlist chain from scratch so removed ranges actually go.
$cmd -N CFWEB 2>/dev/null || $cmd -F CFWEB
local c
for c in $list; do
$cmd -A CFWEB -s "$c" -j ACCEPT
done
$cmd -A CFWEB -j DROP
# -C before adding keeps this idempotent across weekly re-runs.
$cmd -C INPUT -i lo -j ACCEPT 2>/dev/null \
|| $cmd -I INPUT 1 -i lo -j ACCEPT
$cmd -C INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null \
|| $cmd -I INPUT 2 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
$cmd -C INPUT -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW -j CFWEB 2>/dev/null \
|| $cmd -A INPUT -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW -j CFWEB
}
apply iptables "$CF4"
apply ip6tables "$CF6"
echo "cf-origin-lock applied: $(printf '%s\n' "$CF4" | grep -c .) v4 + $(printf '%s\n' "$CF6" | grep -c .) v6 ranges"
[Unit]
Description=Weekly refresh of the Cloudflare origin-lock ranges
[Timer]
# Cloudflare adds and retires ranges occasionally. A stale allowlist silently
# drops edge traffic from any new range, which would look like a partial
# outage, so the list is rebuilt weekly rather than left as a one-off.
OnCalendar=weekly
# Catch up after downtime instead of waiting a full week from the next boot.
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
#!/usr/bin/env bash
#
# Undo cf-origin-lock.sh: stop filtering 80/443 by source and drop the chain.
#
# Leaves the loopback and ESTABLISHED/RELATED accepts in place. They are
# harmless on their own, and removing them from a live box is the one part of
# this that could interrupt an in-flight connection.
set -uo pipefail
for cmd in iptables ip6tables; do
$cmd -D INPUT -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW -j CFWEB 2>/dev/null || true
$cmd -F CFWEB 2>/dev/null || true
$cmd -X CFWEB 2>/dev/null || true
done
echo "cf-origin-lock reverted: 80/443 open to all sources again"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment