When using Cloudflare to hide IP address of the origin server (for example, to protect against DoS attacks), it is important to configure ACLs to allow connections to the origin server only from Cloudflare IPs. However, the list of Cloudflare IP ranges is not static, it changes over time. This post describes how to import this list into nginx automatically.

Notes:

  1. I prefer to put the list into /etc/nginx/include/cloudflare-ips.inc and then include that file from virtual hosts definitions. If you prefer another location, please adjust the path in the scripts below.
  2. Right now, Cloudflare provides separate files for IPv4 and IPv6 ranges. If your system uses only IPv4, you may want to skip IPv6 file.

Shell Script

(\
    ( \
        wget -q https://www.cloudflare.com/ips-v4 -O -; \
        wget -q https://www.cloudflare.com/ips-v6 -O - \
    ) | sed -r 's/^(.+)$/allow \1;/'; \
    echo -e "allow 127.0.0.1;\nallow ::1;\ndeny all;" \
) > /etc/nginx/include/cloudflare-ips.inc && \
systemctl reload nginx

The script is pretty straightforward: first, it downloads both IPv4 and IPv6 list (both lists contain only IP ranges, one range per line); next, it adds allow before every range and ; after; then, it appends three lines to the result: allow 127.0.0.1;, allow ::1; to allow connections from localhost, and deny all; to block all other connection. Finally, the result is put into /etc/nginx/include/cloudflare-ips.inc and nginx gets reloaded.

Simple ansible Playbook

Note: temporary files (lists downloaded from Cloudflare) are stored into /etc/nginx/tmp.

---
- hosts: localhost
  gather_facts: no
  connection: local
  tasks:
    - name: Make sure /etc/nginx/tmp/ exists
      file: path=/etc/nginx/tmp state=directory owner=root group=root mode=0755
    - name: Download IPv4 list
      get_url: url=https://www.cloudflare.com/ips-v4 dest=/etc/nginx/tmp/ip4.txt force=yes
    - name: Download IPv6 list
      get_url: url=https://www.cloudflare.com/ips-v6 dest=/etc/nginx/tmp/ip6.txt force=yes
    - name: Update /etc/nginx/include/cloudflare-ips.inc
      copy: content="{{ (lookup('file', '/etc/nginx/tmp/ip4.txt', rstrip=False) + lookup('file', '/etc/nginx/tmp/ip6.txt', rstrip=False)) | regex_replace('([0-9a-f.:/]+)', 'allow \\1;') + 'allow 127.0.0.1;\nallow ::1;\ndeny all;\n' }}" dest="/etc/nginx/include/cloudflare-ips.inc" force=yes mode=0644 owner=root group=root
      notify: reload nginx
  handlers:
    - name: reload nginx
      service: name=nginx state=reloaded

The playbook first makes sure that the temporary directory for downloaded files exists and has correct permissions, then it downloads both lists from Cloudflare, then with the help of lookup plugin and regex_replace filter, it processes the downloaded files and saves the result to /etc/nginx/include/cloudflare-ips.inc and reloads nginx if necessary.

The playbook can be easily modified to process multiple nodes at once: for that, the copy task should go into a separate hosts block.

The playbook can be extended to perform other tasks as well. For example, I have a dedicated IP for all sites behind Cloudflare, and I disable access to that IP with the firewall (I use csf). Thus I need to update the list of excluded IPs as well.

Piece of cake:

# ...
    - set_fact: ips="{{ lookup('file', '/etc/nginx/tmp/ip4.txt', rstrip=False) + lookup('file', '/etc/nginx/tmp/ip6.txt', rstrip=False) }}" cacheable=false
    - name: Update /etc/nginx/include/cloudflare-ips.inc
      copy: content={{ ips | regex_replace('([0-9a-f.:/]+)', 'allow \\1;') + 'allow 127.0.0.1;\nallow ::1;\ndeny all;\n' }} dest="/etc/nginx/include/cloudflare-ips.inc" force=yes
      notify: reload nginx
    - name: Update CSF exclusions
      blockinfile: block="{{ ips }}" path="{{ item }}" marker="# {mark} Cloudflare"
      with_items:
        - /etc/csf/csf.allow
        - /etc/csf/csf.ignore
      notify: restart csf
# ...
  handlers:
    - name: restart csf
      shell: csf --restartall

How to Import Cloudflare IP List into nginx ACL Automatically
Tagged on:             

Leave a Reply

Your email address will not be published. Required fields are marked *