Cloudflare can sit in front of your server as a reverse proxy: visitors connect to Cloudflare's edge, which serves cached static files, filters malicious requests and forwards the rest to your origin. In this tutorial you will move a domain to Cloudflare, proxy an existing Nginx site on Ubuntu 24.04, encrypt the connection between Cloudflare and your server with an Origin CA certificate in Full (strict) mode, log real visitor IP addresses, and add basic cache and firewall rules. Finally, you will lock the origin so it only accepts web traffic from Cloudflare.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user with sudo privileges.
  • Nginx installed and serving your site on port 80, with a server block for your domain in /etc/nginx/sites-available/.
  • A registered domain name (this guide uses your_domain) and access to its nameserver settings at the registrar.
  • A Cloudflare account. The Free plan covers everything in this guide.
  • UFW enabled, with SSH allowed.

Dashboard menu names in Cloudflare change from time to time. If a label here does not match exactly, use the search box at the top of the Cloudflare dashboard.

Step 1 - Adding your domain to Cloudflare

  1. Log in to the Cloudflare dashboard, choose Add a domain and enter your_domain.
  2. Select the Free plan. Cloudflare scans your current DNS records and imports what it finds.
  3. Review the imported records carefully (Step 2) before continuing.
  4. Cloudflare assigns two nameservers to your zone, for example ada.ns.cloudflare.com and bob.ns.cloudflare.com. The names are different for every account.
  5. At your registrar, replace the existing nameservers with the two Cloudflare ones. If DNSSEC is enabled at the registrar, disable it first; you can re-enable it later from Cloudflare.

Nameserver changes are usually visible within an hour, although they can take up to 24 hours. Check from your server:

dig +short NS your_domain
ada.ns.cloudflare.com.
bob.ns.cloudflare.com.

When the zone becomes active, Cloudflare shows it as Active in the dashboard and sends you an email.

Step 2 - Configuring DNS records

In DNS > Records, each A, AAAA or CNAME record has a proxy status. Proxied (orange cloud) sends traffic through Cloudflare; DNS only (grey cloud) returns your real IP address.

Cloudflare's proxy only handles HTTP and HTTPS. Anything else (SSH, mail, databases, FTP) must use a DNS only record, or it will stop working. A typical setup:

TypeNameContentProxy status
A@your_server_ipProxied
Awwwyour_server_ipProxied
Asshyour_server_ipDNS only
Amailmail_server_ipDNS only
MX@mail.your_domainDNS only

Keep in mind that a DNS only record pointing to the same server reveals the origin IP. If hiding the origin matters to you, connect to SSH by IP address instead of publishing an ssh record.

Verify that the proxied name now resolves to Cloudflare addresses, not your server:

dig +short A your_domain
104.21.x.x
172.67.x.x

Step 3 - Installing a Cloudflare Origin CA certificate

With a proxied record, there are two TLS connections: visitor to Cloudflare (handled automatically with Cloudflare's edge certificate) and Cloudflare to your server. The SSL/TLS encryption modes control the second one:

ModeCloudflare to originUse it?
OffNo HTTPS at allNo
FlexiblePlain HTTPNo: traffic to your server is unencrypted and often causes redirect loops
FullHTTPS, any certificate acceptedOnly temporarily
Full (strict)HTTPS, certificate must be valid and match the hostnameYes

For Full (strict) you need a certificate trusted by Cloudflare on the origin. A free Cloudflare Origin CA certificate is the simplest option: it is valid for up to 15 years and trusted by Cloudflare, but not by browsers, which is fine because browsers never connect to your origin directly.

  1. Go to SSL/TLS > Origin Server and choose Create Certificate.
  2. Keep Generate private key and CSR with Cloudflare, key type RSA (2048), and the hostnames your_domain and *.your_domain.
  3. Choose the validity period and click Create.
  4. Leave the page open: the private key is only shown once.

On the server, create a directory for the files:

sudo install -d -m 0755 /etc/ssl/cloudflare

Paste the Origin Certificate into the certificate file:

sudo nano /etc/ssl/cloudflare/your_domain.pem

Paste the Private Key into the key file, then restrict it to root:

sudo nano /etc/ssl/cloudflare/your_domain.key
sudo chmod 600 /etc/ssl/cloudflare/your_domain.key

Check the certificate and its expiry date:

sudo openssl x509 -in /etc/ssl/cloudflare/your_domain.pem -noout -subject -issuer -enddate
subject=O = "CloudFlare, Inc.", OU = CloudFlare Origin CA, CN = CloudFlare Origin Certificate
issuer=C = US, O = "CloudFlare, Inc.", OU = CloudFlare Origin SSL Certificate Authority, L = San Francisco, ST = California
notAfter=Sep 21 09:00:00 2041 GMT

Step 4 - Configuring Nginx for HTTPS

Open your site's server block:

sudo nano /etc/nginx/sites-available/your_domain

Replace its contents with a block that listens on 443 with the Origin CA certificate. Adjust root (or your location blocks, for example proxy_pass to an application) to match your existing site:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your_domain www.your_domain;

    ssl_certificate     /etc/ssl/cloudflare/your_domain.pem;
    ssl_certificate_key /etc/ssl/cloudflare/your_domain.key;

    root /var/www/your_domain;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~* \.(css|js|jpg|jpeg|png|gif|svg|webp|ico|woff2)$ {
        expires 30d;
        add_header Cache-Control "public";
    }
}

There is no port 80 block: Cloudflare will redirect visitors to HTTPS at the edge (next step) and only connect to your origin on 443. The expires directive sets Cache-Control: max-age on static files, which Cloudflare respects when deciding how long to keep them at the edge.

Test the configuration and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Step 5 - Enabling Full (strict) and HTTPS redirects

In the Cloudflare dashboard:

  1. Go to SSL/TLS > Overview, choose Configure and select Full (strict).
  2. Go to SSL/TLS > Edge Certificates and enable Always Use HTTPS. Set Minimum TLS Version to TLS 1.2.

Verify from your workstation or the server that requests pass through Cloudflare and the redirect works:

curl -sI http://your_domain | grep -iE '^(HTTP|location|server)'
curl -sI https://your_domain | grep -iE '^(HTTP|server|cf-ray|cf-cache-status)'
HTTP/1.1 301 Moved Permanently
Location: https://your_domain/
Server: cloudflare
HTTP/2 200
server: cloudflare
cf-cache-status: DYNAMIC
cf-ray: 8cxxxxxxxxxxxxxx-MAD

server: cloudflare and a cf-ray header confirm the request went through the proxy. DYNAMIC means the HTML page was not cached, which is Cloudflare's default for HTML.

Step 6 - Logging real visitor IP addresses

Behind the proxy, every request reaches Nginx from a Cloudflare address, so your access logs, rate limits and applications all see Cloudflare instead of the visitor. Cloudflare sends the original address in the CF-Connecting-IP header, and Nginx's realip module (included in Ubuntu's nginx package) can use it, as long as you only trust that header from Cloudflare's own ranges.

Generate the configuration from Cloudflare's published IP lists:

{
  echo "# Cloudflare IP ranges, from https://www.cloudflare.com/ips/"
  for ip in $(curl -fsS https://www.cloudflare.com/ips-v4) $(curl -fsS https://www.cloudflare.com/ips-v6); do
    echo "set_real_ip_from $ip;"
  done
  echo "real_ip_header CF-Connecting-IP;"
} | sudo tee /etc/nginx/conf.d/cloudflare-realip.conf > /dev/null

Check the result, test and reload:

head -n 3 /etc/nginx/conf.d/cloudflare-realip.conf
sudo nginx -t && sudo systemctl reload nginx
# Cloudflare IP ranges, from https://www.cloudflare.com/ips/
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;

Load your site in a browser and look at the last line of the access log. It should show your own public IP address, not a Cloudflare one:

sudo tail -n 1 /var/log/nginx/access.log

Step 7 - Restricting the origin to Cloudflare

Anyone who discovers your server's IP address can still connect to it directly and bypass Cloudflare's firewall. Allow HTTPS only from Cloudflare's ranges with UFW:

for ip in $(curl -fsS https://www.cloudflare.com/ips-v4) $(curl -fsS https://www.cloudflare.com/ips-v6); do
  sudo ufw allow proto tcp from "$ip" to any port 443 comment 'Cloudflare'
done

Remove any broader web rules you had before. List the numbered rules and delete the old ones (for example Nginx Full, Nginx HTTP or 80/tcp) by name:

sudo ufw status numbered
sudo ufw delete allow 'Nginx Full'

Confirm that only SSH and the Cloudflare ranges remain:

sudo ufw status | head -n 8
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
443/tcp                    ALLOW       173.245.48.0/20            # Cloudflare
443/tcp                    ALLOW       103.21.244.0/22            # Cloudflare
443/tcp                    ALLOW       103.22.200.0/22            # Cloudflare

Step 8 - Adding cache and security rules

By default Cloudflare caches static files by extension (images, CSS, JavaScript, fonts) and never caches HTML. The rules below cover two common needs. Create them under Caching > Cache Rules and Security > Security rules (called WAF > Custom rules in older dashboards).

Never cache the admin area and logged-in pages. Create a cache rule:

  • Expression: starts_with(http.request.uri.path, "/admin")
  • Cache eligibility: Bypass cache

Cache a static directory longer at the edge. If your site serves versioned assets from /static/:

  • Expression: starts_with(http.request.uri.path, "/static/")
  • Cache eligibility: Eligible for cache
  • Edge TTL: Ignore cache-control header and use this TTL, 30 days

Challenge suspicious login traffic. Create a custom security rule, for example for a WordPress login page:

  • Expression: http.request.uri.path eq "/wp-login.php" and not ip.src in {your_office_ip}
  • Action: Managed Challenge

In addition, the Free plan applies the Cloudflare Free Managed Ruleset automatically, and you can turn on Bot Fight Mode under Security > Bots. Free plans also include one rate limiting rule with short block durations; longer blocks require a paid plan.

After deploying new CSS or JavaScript without changing the file names, purge the cache so visitors get the new files. Create an API token with the Cache Purge permission for the zone, copy the Zone ID from the zone's Overview page, and run:

curl -fsS -X POST "https://api.cloudflare.com/client/v4/zones/your_zone_id/purge_cache" \
  -H "Authorization: Bearer your_api_token" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://your_domain/static/app.css","https://your_domain/static/app.js"]}'
{"success":true,"errors":[],"messages":[],"result":{"id":"your_zone_id"}}

Use {"purge_everything":true} as the payload to clear the whole zone.

Check that a static file is served from the cache. The first request is usually a MISS and the next one a HIT:

curl -sI https://your_domain/static/app.css | grep -i cf-cache-status
cf-cache-status: HIT

Troubleshooting

Error 521 (Web server is down): Cloudflare could not open a connection to your origin. Check that Nginx is running with sudo systemctl status nginx, that it listens on 443 with sudo ss -tlnp | grep ':443', and that UFW allows the Cloudflare ranges.

Error 522 (Connection timed out): packets are being dropped, usually by a firewall that does not include all current Cloudflare ranges. Compare sudo ufw status with https://www.cloudflare.com/ips/.

Error 526 (Invalid SSL certificate): Full (strict) is on but the origin certificate is not valid for the hostname or has expired. Check the files referenced in the Nginx block and run the openssl x509 command from Step 3.

Too many redirects: the SSL/TLS mode is set to Flexible while the origin redirects HTTP to HTTPS. Use Full (strict).

Access logs still show Cloudflare IPs: /etc/nginx/conf.d/cloudflare-realip.conf is not loaded. Confirm that /etc/nginx/nginx.conf includes /etc/nginx/conf.d/*.conf inside the http block and that you reloaded Nginx.

Conclusion

Your site now runs behind Cloudflare with encrypted traffic end to end, real visitor IPs in the Nginx logs, an origin that only accepts HTTPS from Cloudflare, and cache and security rules tuned for your paths. Next, consider enabling Authenticated Origin Pulls so Nginx also verifies Cloudflare's client certificate, automating the Cloudflare IP list update with a systemd timer, or managing your DNS records and rules as code with Terraform.