Last active
March 14, 2026 02:37
-
-
Save groundcat/6803c48855869aa0fecc3416d6eb985a to your computer and use it in GitHub Desktop.
Restricts nginx to accept connections ONLY from Cloudflare IPs (v4 + v6)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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