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:
- 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. - 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