Skip to content

Instantly share code, notes, and snippets.

@groundcat
Last active March 14, 2026 02:37
Show Gist options
  • Select an option

  • Save groundcat/6803c48855869aa0fecc3416d6eb985a to your computer and use it in GitHub Desktop.

Select an option

Save groundcat/6803c48855869aa0fecc3416d6eb985a to your computer and use it in GitHub Desktop.
Restricts nginx to accept connections ONLY from Cloudflare IPs (v4 + v6)
#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# cf-nginx-restrict.sh
# Restricts nginx to accept connections ONLY from Cloudflare IPs (v4 + v6),
# while also allowing localhost / loopback access.
#
# Applies globally across ALL vhosts via /etc/nginx/conf.d/cloudflare-only.conf
# Safe to re-run: always fetches fresh IPs, diffs, updates only if changed.
# Requires: nginx, curl. Tested on Debian/Ubuntu.
# Run as root.
# ------------------------------------------------------------------------------
set -euo pipefail
CF_V4_URL="https://www.cloudflare.com/ips-v4/"
CF_V6_URL="https://www.cloudflare.com/ips-v6/"
OUT_FILE="/etc/nginx/conf.d/cloudflare-only.conf"
TMP_FILE="$(mktemp /tmp/cloudflare-only.conf.XXXXXX)"
BACKUP_FILE="$(mktemp /tmp/cloudflare-only.backup.XXXXXX)"
cleanup() {
rm -f "$TMP_FILE"
}
trap cleanup EXIT
# --- Sanity checks ------------------------------------------------------------
if [[ $EUID -ne 0 ]]; then
echo "[ERROR] This script must be run as root." >&2
exit 1
fi
for cmd in curl nginx grep mv chmod mktemp date; do
if ! command -v "$cmd" &>/dev/null; then
echo "[ERROR] Required command not found: $cmd" >&2
exit 1
fi
done
# --- Fetch IP ranges ----------------------------------------------------------
echo "[INFO] Fetching Cloudflare IP ranges..."
CF_V4="$(curl -fsSL --max-time 15 "$CF_V4_URL")" || {
echo "[ERROR] Failed to fetch IPv4 ranges from $CF_V4_URL" >&2
exit 1
}
CF_V6="$(curl -fsSL --max-time 15 "$CF_V6_URL")" || {
echo "[ERROR] Failed to fetch IPv6 ranges from $CF_V6_URL" >&2
exit 1
}
if [[ -z "$CF_V4" || -z "$CF_V6" ]]; then
echo "[ERROR] One or both IP lists returned empty. Aborting." >&2
exit 1
fi
V4_COUNT=$(echo "$CF_V4" | grep -c .)
V6_COUNT=$(echo "$CF_V6" | grep -c .)
echo "[INFO] Retrieved $V4_COUNT IPv4 and $V6_COUNT IPv6 ranges."
# --- Generate config ----------------------------------------------------------
{
echo "# Cloudflare-only access with localhost allowed — auto-generated by cf-nginx-restrict.sh"
echo "# Last updated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo "# Source: $CF_V4_URL $CF_V6_URL"
echo ""
echo "# Allow localhost / loopback"
echo "allow 127.0.0.1;"
echo "allow ::1;"
echo ""
echo "# Allow only Cloudflare IPs"
while IFS= read -r ip; do
[[ -z "$ip" ]] && continue
echo "allow ${ip};"
done <<< "$CF_V4"
while IFS= read -r ip; do
[[ -z "$ip" ]] && continue
echo "allow ${ip};"
done <<< "$CF_V6"
echo ""
echo "# Deny everything else"
echo "deny all;"
} > "$TMP_FILE"
# --- Diff: skip reload if nothing changed -------------------------------------
if [[ -f "$OUT_FILE" ]]; then
existing_stripped="$(grep -v "^# Last updated:" "$OUT_FILE" || true)"
new_stripped="$(grep -v "^# Last updated:" "$TMP_FILE")"
if [[ "$existing_stripped" == "$new_stripped" ]]; then
echo "[INFO] Config is already up to date. No changes needed."
exit 0
fi
cp "$OUT_FILE" "$BACKUP_FILE"
echo "[INFO] Existing config found — updating with new IP ranges."
else
rm -f "$BACKUP_FILE"
echo "[INFO] No existing config found — creating $OUT_FILE."
fi
# --- Install config -----------------------------------------------------------
mv "$TMP_FILE" "$OUT_FILE"
chmod 644 "$OUT_FILE"
echo "[INFO] Written: $OUT_FILE"
# --- Test nginx config --------------------------------------------------------
echo "[INFO] Testing nginx configuration..."
if ! nginx -t; then
echo "[ERROR] nginx config test failed." >&2
if [[ -f "$BACKUP_FILE" ]]; then
echo "[INFO] Restoring previous config..."
mv "$BACKUP_FILE" "$OUT_FILE"
chmod 644 "$OUT_FILE"
if nginx -t; then
echo "[INFO] Previous config restored successfully." >&2
else
echo "[ERROR] Previous config was restored, but nginx -t still fails." >&2
echo "[ERROR] Review your overall nginx configuration manually." >&2
fi
else
echo "[ERROR] No previous config existed, so nothing could be restored." >&2
fi
exit 1
fi
rm -f "$BACKUP_FILE"
# --- Reload nginx -------------------------------------------------------------
echo "[INFO] Reloading nginx..."
if systemctl is-active --quiet nginx; then
systemctl reload nginx
echo "[SUCCESS] nginx reloaded. Only Cloudflare IPs and localhost are now allowed on all vhosts."
else
echo "[WARN] nginx does not appear to be running. Start it with: systemctl start nginx"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment