Automated Cloudflare ACL scripts for Nginx & Traefik

Generate an ACL file containing Cloudflare IPv4 and IPv6 ranges for both Nginx and Traefik. Automate updates with cron.

traefik
traefik

Recently I decided to start using Cloudflare in proxy mode again. This lead to needing to wall off the back-end hosts from getting hit by requests outside of Cloudflare's IP ranges.

Ideally if using Cloudflare in proxy mode, only Cloudflare should be able to connect.

There are a few other ways to handle this such as mTLS (authenticated origin pulls) or cloudflared/cloudflare tunnel(s).

For cloudflare tunnels see:

Using Cloudflare Tunnels to Expose Local Containers
Use Cloudflare Tunnels to expose local containers to the internet.

For simplicity at the moment, I'm going to solve this with a simple ACL.

Nginx

Place this script somewhere that cron has permissions to execute and make the script executable with 'chmod +x'.

#!/bin/bash
# vars
output_directory='/etc/nginx/snippets'
output_file='cloudflare_CIDR_ACL.conf'

set -e

cf_ips() {
  echo "# https://www.cloudflare.com/ips"

  for type in v4 v6; do
    echo "# IP$type"
    curl -s "https://www.cloudflare.com/ips-$type" | sed "s|^|allow |g" | sed "s|\$|;|g"
    echo
  done

  echo "# Generated at $(LC_ALL=C date)"
}

cf_ips > $output_directory/$output_file
(cf_ips && echo "deny all; # deny all remaining ips") > $output_directory/$output_file

generate-nginx-cloudflare-allow.sh

Modify the output directory and output file as needed for your environment.

Example generated Nginx ACL snippet:

# https://www.cloudflare.com/ips
# IPv4
allow 173.245.48.0/20;
allow 103.21.244.0/22;
allow 103.22.200.0/22;
allow 103.31.4.0/22;
allow 141.101.64.0/18;
allow 108.162.192.0/18;
allow 190.93.240.0/20;
allow 188.114.96.0/20;
allow 197.234.240.0/22;
allow 198.41.128.0/17;
allow 162.158.0.0/15;
allow 104.16.0.0/13;
allow 104.24.0.0/14;
allow 172.64.0.0/13;
allow 131.0.72.0/22;
# IPv6
allow 2400:cb00::/32;
allow 2606:4700::/32;
allow 2803:f800::/32;
allow 2405:b500::/32;
allow 2405:8100::/32;
allow 2a06:98c0::/29;
allow 2c0f:f248::/32;
# Generated at Wed Oct 30 00:00:06 UTC 2024
deny all; # deny all remaining ips

cloudflare_CIDR_ACL.conf

Run the script, then add the resulting snippet to your nginx config - see example nginx config using this generated ACL:

server {
    listen 80;
    listen [::]:80;
    server_name bitbysystems.com;
    return 301 https://$server_name$request_uri;
    include /etc/nginx/snippets/cloudflare_CIDR_ACL.conf;
}
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name bitbysystems.com;
    ssl_certificate /etc/letsencrypt/live/bitbysystems.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/bitbysystems.com/privkey.pem;
    include /etc/nginx/snippets-enabled/errors.conf;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    ssl_session_tickets off;

    # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
    ssl_dhparam /etc/nginx/dhparam.pem;

    # intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;

    include /etc/nginx/snippets/cloudflare_CIDR_ACL.conf; 

    location / {
    proxy_pass http://172.16.0.37:2368;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;
    }
}

bitbysystems.com.conf

I use cron to update this ACL nightly.

0 0 * * * sh /etc/nginx/scripts/generate-nginx-cloudflare-allow.sh

Reload nginx

Nginx does not apply configurations or changes dynamically. This is one huge strong point of traefik. On each interval that we are updating the ACL file, we also should reload nginx.

Reloading nginx is graceful and does not disrupt existing connections.

#!/bin/bash

# Test nginx configuration
if nginx -t; then
    echo "Configuration test passed, reloading Nginx..."
    nginx -s reload
    if [ $? -eq 0 ]; then
        echo "Nginx reloaded successfully"
        exit 0
    else
        echo "Failed to reload Nginx"
        exit 1
    fi
else
    echo "Configuration test failed, not reloading Nginx"
    exit 1
fi

test-reload.sh

Schedule this script after the acl script, or run in one after another.

Example cron including the nginx reload script:

0 0 * * * sh /etc/nginx/scripts/generate-nginx-cloudflare-allow.sh && sh /etc/nginx/scripts/test-reload.sh

Additionally, a tool such as cronitor or healthchecks.io would be a good addition to monitor these tasks.

Traefik

Place this script somewhere that CRON has permissions and make executable with 'chmod +x'.

#!/bin/bash

# Variables
output_directory='/opt/traefik/conf'
output_file='cloudflare-middleware.yml'

# Function to generate the Traefik configuration
generate_traefik_config() {
    echo "# Auto-generated Cloudflare middleware configuration"
    echo "# Last updated: $(LC_ALL=C date)"
    echo "http:"
    echo "  middlewares:"
    echo "    cloudflare-only:"
    echo "      ipWhiteList:"
    echo "        sourceRange:"
    
    # Get IPv4 and IPv6 addresses
    for type in v4 v6; do
        # Add comment for IP version
        echo "          # IP$type"
        curl -s "https://www.cloudflare.com/ips-$type" | sed 's/^/          - "/' | sed 's/$/"/'
        echo
    done
}

# Generate the configuration file
generate_traefik_config > "$output_directory/$output_file"

traefik-cloudflare-acl-generator.sh

Example generated ACL file:

# Auto-generated Cloudflare middleware configuration
# Last updated: Wed Oct 30 05:03:53 UTC 2024
http:
  middlewares:
    cloudflare-only:
      ipWhiteList:
        sourceRange:
          - "173.245.48.0/20"
          - "103.21.244.0/22"
          - "103.22.200.0/22"
          - "103.31.4.0/22"
          - "141.101.64.0/18"
          - "108.162.192.0/18"
          - "190.93.240.0/20"
          - "188.114.96.0/20"
          - "197.234.240.0/22"
          - "198.41.128.0/17"
          - "162.158.0.0/15"
          - "104.16.0.0/13"
          - "104.24.0.0/14"
          - "172.64.0.0/13"
          - "2400:cb00::/32"
          - "2606:4700::/32"
          - "2803:f800::/32"
          - "2405:b500::/32"
          - "2405:8100::/32"
          - "2a06:98c0::/29"

middleware-cloudflare-acl.yml

I use cron to update this ACL nightly.

0 0 * * * /opt/traefik/scripts/traefik-cloudflare-acl-generator.sh

Traefik Configuration

Ensure that in 'traefik.yml' the file provider is configured. Below is a small section of my traefik.yml. I run traefik as a container, so /etc/traefik is a bind mount to a host directory where I store config files.

...
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    directory: "/etc/traefik"
    watch: true
...    

traefik.yml

Then ensure the script is set to output the file as a middleware YAML file to your Traefik config directory.

Traefik, unlike nginx, does not need reloaded when changes are made due to its dynamic nature.