Preface

I used to find Nginx configuration cumbersome, and after using Cloudflare Tunnel and enjoying the sweet automatic reverse proxy and HTTPS, I didn’t want to touch Nginx. However, Cloudflare Tunnel is not a silver bullet; its support for certain services can be finicky.

Recently, for larger services, I tried combining traditional Nginx reverse proxy with Cloudflare CDN for access. Thanks to Cloudflare’s Origin Certificates, deploying fully-fledged HTTPS became simple without needing CertBot or acme.sh, while Cloudflare handles automatic issuance, deployment, and updating of Edge Certificates.

In this article, I will document my straightforward practice, which also helped me get familiar with Nginx usage—it is actually quite simple.1

Image 1: Overview

SSH Security

SSH is the gateway to our servers. There are two ways to protect access to it:

  1. Close the SSH port and rely on Cloudflare Tunnel for SSH access.
  2. Keep the SSH port open with proper SSH hardening.

Method 1 is undoubtedly the most secure 2, but SSH quality depends heavily on Cloudflare service uptimes: e.g., Cloudflare Tunnel degradations, local cloudflared panics 🥹, etc.

Method 2 involves standard SSH hardening:

  • Change the default SSH port

  • Forbid empty password logins

  • Disable root logins

  • Use public/private keys for SSH access (hardware security keys preferred) 3

  • Configure Fail2ban

Configure UFW

Generally, besides the SSH port, we only open HTTP (80) and HTTPS (443). First, block other ports in the cloud dashboard, then apply IP filtering.

UFW is a frontend that simplifies configuring the underlying Linux firewall. Extremely easy to use, installed by default in Ubuntu, but requires manual install on Debian:

1
sudo apt update && sudo apt install ufw -y

Step 1 is to allow the SSH port (replace 9912 with your actual SSH port):

1
sudo ufw allow 9912/tcp

Step 2, deny all incoming traffic:

1
sudo ufw default deny incoming

Step 3, allow ports 80 and 443 exclusively for Cloudflare IPs. Manual typing is tedious, create a script instead:

1
touch ufw-only-cf.sh

Edit the script:

1
vim ufw-only-cf.sh

Write the following items and save:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash

for ipv4 in `curl -s https://www.cloudflare.com/ips-v4 | tee ips-v4`
do
    sudo ufw allow from $ipv4 to any port 80
    sudo ufw allow from $ipv4 to any port 443
done

for ipv6 in `curl -s https://www.cloudflare.com/ips-v6 | tee ips-v6`
do
    sudo ufw allow from $ipv6 to any port 80
    sudo ufw allow from $ipv6 to any port 443
done

Execute the script:

1
sudo chmod +x ufw-only-cf.sh && ./ufw-only-cf.sh

Enable UFW:

1
sudo ufw enable

Reload UFW rules:

1
sudo ufw reload

Configure Nginx

To ensure our services still obtain the visitor’s real IP through Cloudflare CDN, Nginx configuration needs tweaking. First, back up the default file:

1
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak

Edit the default Nginx configuration file:

1
sudo vim /etc/nginx/nginx.conf

Find the section below:

1
2
3
http {
  ...
}

Add the following fields: 4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# modified
# https://www.cloudflare.com/ips-v4
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;

# https://www.cloudflare.com/ips-v6
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;

# CF-Connecting-IP is recommend
# real_ip_header X-Forwarded-For;
real_ip_header CF-Connecting-IP;

Test the Nginx configuration:

1
sudo nginx -t

If successful, reload Nginx:

1
sudo systemctl reload nginx

Origin Certificates

Generate an Origin Server SSL certificate on Cloudflare (valid up to 15 years max), trusted only by Cloudflare itself.

Image 2: Origin SSL

Then install it on your Web Server. Taking Nginx reverse proxy for Plausible as an example:

1
cd /etc/nginx

Create a cert directory where all certificates will reside:

1
sudo mkdir cert && cd $_

Create certificate files for the domain stats.dejavu.moe:

1
sudo touch stats.dejavu.moe.pem stats.dejavu.moe.key

Edit public and private key files separately, insert the origin certificate data inside and save:

1
2
sudo vim stats.dejavu.moe.pem
sudo vim stats.dejavu.moe.key

Create the Nginx config file:

1
sudo vim /etc/nginx/conf.d/stats.dejavu.moe.conf

Origin cert installation layout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
server {
    listen 80;
    listen [::]:80;
    server_name stats.dejavu.moe;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name stats.dejavu.moe;
    
    # Specify origin server certificate paths
    ssl_certificate /etc/nginx/cert/stats.dejavu.moe.pem;
    ssl_certificate_key /etc/nginx/cert/stats.dejavu.moe.key;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect off;
    }
}

Check the config; if clean, reload Nginx:

1
2
sudo nginx -t
sudo systemctl reload nginx

Finally, resolve the domain to your server IP in Cloudflare panel setups and set SSL/TLS Encryption Mode to Full (Strict).

References


  1. Never know unless you try 🤤↩︎

  2. Assuming you fully trust Cloudflare↩︎

  3. e.g., Yubikey, Canokey, FeiTian Key…↩︎

  4. Cloudflare IP ranges rarely diverge; as of writing, the last change was April 8, 2021↩︎