MHOSTMHOST

Cloudflare in front of your VPS

Cloudflare is a reverse proxy and CDN that sits between visitors and your VPS: requests land in Cloudflare's network first, and only Cloudflare talks to your actual server (the origin). In this article you'll add a domain to Cloudflare, sort out proxied vs. DNS only records, turn on Full (strict) SSL/TLS with a real certificate, set up basic caching, learn how to recover the visitor's real IP instead of Cloudflare's, and see what to watch out for when issuing a certbot certificate while the domain is already proxied. The payoff is two practical effects: static assets are served faster thanks to the CDN, and your VPS's real IP is hidden from visitors and most scanners, which absorbs a chunk of basic DDoS attacks right at Cloudflare's edge.

What you'll need

  • A domain registered with any registrar, and access to its management panel — specifically, wherever you change nameservers.
  • A working site on your mHost VPS, already reachable over HTTP(S) — for example, set up per the Nginx or Apache articles. If the domain isn't pointed at the server yet, see "How to point a domain at your server"; if you need a refresher on what A/AAAA/CNAME and NS records are, see "DNS records: A, AAAA, CNAME, and more".
  • Ideally, an already-issued Let's Encrypt certificate — see "Free SSL with Let's Encrypt (certbot)". The rest of the article explains how it interacts with Cloudflare, but you can start without one too.
  • A free Cloudflare account — everything covered in this article (proxying, SSL/TLS, basic caching, hiding your IP) is available on the Free plan.
  • SSH access as root or a user with sudo — for setting up the realip module and, optionally, the firewall.
Tip: even if the domain is already pointed straight at the VPS through your registrar's panel (per the domain-connection article), that work isn't wasted — adding it to Cloudflare just moves DNS management to Cloudflare's own nameservers, and you'll recreate the same A record, just inside Cloudflare's panel instead.

Step 1. Add the domain to Cloudflare and switch nameservers

  1. Sign up or log in at dash.cloudflare.com.
  2. Click Add a domain (on some dashboard versions, Onboard a domain) and enter the domain without www or a protocol, e.g. example.com.
  3. Pick a plan — Free is enough for everything covered in this article.
  4. Cloudflare scans the domain's current DNS records and offers to import them. Review the list carefully — especially mail records (MX, TXT with SPF/DKIM/DMARC): if the scanner misses or mangles something, email will break once you switch nameservers. Add any missing records by hand before switching.
  5. Cloudflare assigns two nameservers like xxxx.ns.cloudflare.com — go to your domain registrar's panel and replace the current nameservers with those two.
Important: if DNSSEC is enabled at your previous registrar or DNS provider, disable it before switching nameservers — otherwise the domain can become completely unreachable while things are out of sync. You can re-enable DNSSEC afterward, through Cloudflare itself, once the domain is active.

The nameserver switch doesn't take effect instantly — usually anywhere from a few minutes up to 24 hours, while the change propagates to resolvers. You can check which nameservers are currently visible from outside with:

bash
dig NS example.com +short

Once the answer shows two addresses like *.ns.cloudflare.com, and the domain's status in the Cloudflare dashboard switches to Active, you can move on to configuring records.

Done: the domain's status in the Cloudflare dashboard is Active.

Step 2. Proxied vs. DNS only: the orange and grey cloud

Under DNS → Records, every record has a "cloud" toggle:

  • Proxied (orange cloud) — the request goes through Cloudflare's network: visitors see Cloudflare's IP address, not your VPS's real IP; caching, WAF, basic DDoS protection, and Cloudflare's other features all apply to this record.
  • DNS only (grey cloud) — Cloudflare just answers with the real IP from the record, like a plain DNS resolver, no proxy involved; Cloudflare's features (caching, WAF, hiding your IP) don't apply to this record.

Only A, AAAA, and CNAME records can be proxied — other types (MX, TXT, NS, SRV, and so on) don't even have the toggle; they're always DNS only. That's a Cloudflare-wide restriction, not a setting.

  • Turn on Proxied for your site's main records (example.com, www.example.com) — that's what gets you the CDN and IP hiding.
  • Leave mail records (MX) and records used by mail services (SPF/DKIM/DMARC-style TXT, autodiscover/autoconfig CNAME) as DNS only: Cloudflare doesn't proxy mail traffic, and a proxied CNAME can break verification with your mail provider.
  • Utility subdomains you connect to directly (say, a separate non-HTTP service) are also best left as DNS only.
Tip: once a record is Proxied, a plain dig/nslookup against the domain no longer shows your VPS's real IP — a nice side effect, but not a substitute for actually securing the server. Set up the firewall and SSH hardening separately too — see "The ufw firewall" and "Server security checklist".

Step 3. SSL/TLS mode: turn on Full (strict)

Under SSL/TLS → Overview, choose the encryption mode between Cloudflare and your origin server:

  • Off — no encryption at all; all traffic, including Cloudflare → origin, travels as plain text. Don't use this.
  • Flexible — visitor ↔ Cloudflare can be HTTPS, but Cloudflare ↔ origin is always plain HTTP.
  • Full — Cloudflare talks to the origin over the same protocol the visitor used, including HTTPS, but doesn't validate the origin's certificate — a self-signed one works fine.
  • Full (strict) — the same as Full, but Cloudflare also verifies that the origin's certificate is genuine: issued by a public CA (Let's Encrypt, for example) or by Cloudflare Origin CA.

Cloudflare recommends Full or Full (strict) to rule out unencrypted or unauthenticated connections to the origin. If Let's Encrypt is already set up on the VPS (see "Free SSL with Let's Encrypt (certbot)"), go straight to Full (strict) — it's the most secure option and costs nothing beyond the certificate you already have.

Important: if the origin already redirects HTTP to HTTPS (certbot's --nginx/--apache plugin adds this automatically — see the Let's Encrypt article), don't pick Flexible. In that mode, Cloudflare only ever reaches the origin over HTTP, the origin redirects back to HTTPS, Cloudflare connects again over HTTP — and you get an infinite redirect loop, showing up in the browser as something like ERR_TOO_MANY_REDIRECTS. The fix is to switch the mode to Full or Full (strict), not to remove the redirect on the origin.

If the origin doesn't have a certificate at all yet, set the mode to Full for now (nothing to validate yet for strict) or temporarily switch the record to DNS only (step 2) — then issue a certificate per step 4.

Step 4. The origin certificate and certbot behind the proxy

If Let's Encrypt was already set up on the VPS before you added Cloudflare, there's nothing more to do here — skip to step 5. Below is what to watch for if you still need to obtain or renew a certificate — say, for a new subdomain — after the record has already become Proxied.

Certbot's HTTP-01 validation (--nginx/--apache/--webroot) generally works fine through a proxied domain too: Let's Encrypt's request to http://example.com/.well-known/acme-challenge/... lands on Cloudflare's edge, which forwards it to your origin on port 80. But two standard Cloudflare mechanisms can break this:

  • Always Use HTTPS (SSL/TLS → Edge Certificates) — redirects to HTTPS right at the edge, before the request ever reaches the origin. If the origin doesn't have a valid certificate yet, a fresh HTTP-01 issuance won't get through at that point.
  • A Cache Rule with "Cache Everything" for the whole site — can cache a stale response for the /.well-known/acme-challenge/... path, so a renewal's re-validation gets served an outdated token.

Two approaches that work:

  • Simplest — while issuing or renewing the certificate, switch the record back to DNS only (grey cloud, step 2), run certbot as usual (see "Free SSL with Let's Encrypt (certbot)"), and switch it back to Proxied once the certificate is ready.
  • More reliable for auto-renewal — switch certbot to DNS-01 validation via the official certbot-dns-cloudflare plugin. It proves domain ownership through a TXT record created via the Cloudflare API, and doesn't care whether the record is proxied or not.

To set up DNS-01, first create an API token in Cloudflare: My Profile → API Tokens → Create Token, the Edit zone DNS template, and under Zone Resources pick the specific domain rather than "all zones." Copy the token — the dashboard only shows it once.

Next, on the VPS (certbot should already be installed via snap, as in the Let's Encrypt article):

bash
sudo snap install certbot-dns-cloudflare
sudo snap set certbot trust-plugin-with-root=ok
sudo snap connect certbot:plugin certbot-dns-cloudflare

Check that the plugin connected:

bash
certbot plugins

dns-cloudflare should show up in the list. Save the token to a file readable only by root:

bash
sudo mkdir -p /root/.secrets/certbot
sudo tee /root/.secrets/certbot/cloudflare.ini > /dev/null <<'EOF'
dns_cloudflare_api_token = <CLOUDFLARE_API_TOKEN>
EOF
sudo chmod 600 /root/.secrets/certbot/cloudflare.ini

Replace <CLOUDFLARE_API_TOKEN> with the real token from the Cloudflare dashboard, then request the certificate:

bash
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 60 \
  -d example.com -d www.example.com
Tip: --dns-cloudflare-propagation-seconds 60 gives the TXT record time to propagate across Cloudflare's DNS before validation runs; the default (10 seconds) usually works too, but 60 is safer. If this domain already had a certificate issued via --nginx/--apache, certbot reuses the same certificate (same path, /etc/letsencrypt/live/example.com/) and updates /etc/letsencrypt/renewal/example.com.conf — you won't need to touch the nginx/Apache configuration, and the auto-renewal systemd timer (see the Let's Encrypt article) will use DNS-01 from then on.

certonly mode only obtains the certificate — it doesn't touch your web server's configuration. If you haven't set up HTTPS in nginx/Apache by hand yet, do that following "Free SSL with Let's Encrypt (certbot)", pointing it at /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem.

Tip: instead of a publicly trusted certificate for the Cloudflare ↔ origin leg, you can use a Cloudflare Origin CA certificate (SSL/TLS → Origin Server → Create Certificate) — it's free, valid for up to 15 years, and doesn't need certbot at all. The catch: browsers don't trust it directly, it's only valid for Cloudflare's own validation in Full (strict) mode — anyone who reaches the origin directly, bypassing Cloudflare, will get a certificate error. Only use it if you're sure all traffic flows exclusively through proxied records.

Check the result after either approach:

bash
curl -I https://example.com/
Done: the response is HTTP/2 200 with no certificate warnings — the Full (strict) mode from step 3 is working.

Step 5. Basic caching

By default, with no configuration at all, Cloudflare already caches static content by file extension — images (jpg, png, gif, webp), video (mp4, webm), CSS, JS, fonts (woff, woff2), archives. HTML and JSON are not cached by default — Cloudflare goes by the origin's Cache-Control/Expires headers, and falls back to a default TTL (around 2 hours for successful responses) if there aren't any.

To cache more — say, HTML for static pages, or to guarantee a TTL regardless of the origin's headers — use Cache Rules (Rules → Overview, or on some dashboard versions Caching → Cache Rules; this is the current replacement for the older Page Rules):

  • Set a condition, e.g. "URI Path starts with /blog/" or "Hostname equals example.com."
  • For the action, pick Cache eligibility: Eligible for cache and set an Edge TTL — say, 2 hours.
  • For a fully static site with no accounts or cookies, you can apply Cache Everything to the whole domain — but don't do that for sites with logins, a cart, or other personal content: you risk serving one visitor a page cached for someone else.

Also worth knowing about:

  • Browser Cache TTL (Caching → Configuration) — a separate setting: how long the resource stays in the visitor's own browser cache, not to be confused with Cloudflare's edge cache.
  • Development Mode (same place, Caching → Configuration) — temporarily disables Cloudflare's cache for 3 hours; handy while testing changes on the origin so you don't have to wait for the cache to expire.
  • Purge Cache — after deploying updates on the origin, it's worth purging the cache ("Purge Everything" or by specific URL), otherwise some visitors will keep seeing the old version for a while.
Tip: if you created a blanket Cache Everything rule, remember step 4 — the /.well-known/acme-challenge/... path shouldn't be cached, or certbot renewals can break on a stale response.

Step 6. The visitor's real IP: the CF-Connecting-IP header

Once a record is Proxied, nginx/Apache logs and every application on the VPS will see Cloudflare's edge IP instead of the visitor's real one — that breaks logs, geo-IP, rate limiting, and tools like fail2ban that rely on the IP address.

Cloudflare adds a `CF-Connecting-IP` header to every proxied request — it holds exactly one real visitor IP address. There's also True-Client-IP (the same thing, but Enterprise plan only) and the standard X-Forwarded-For (a comma-separated list, less convenient for this purpose) — use CF-Connecting-IP specifically to recover the real IP.

Nginx: the ngx_http_realip_module module

The ngx_http_realip_module module is already built into the standard nginx package from the Ubuntu/Debian/AlmaLinux/Rocky repositories — nothing extra to install (you can check with nginx -V 2>&1 | grep -o http_realip_module). Generate a config with Cloudflare's current IP ranges — the official list is published on cloudflare.com:

bash
{
  for ip in $(curl -s https://www.cloudflare.com/ips-v4); do
    echo "set_real_ip_from $ip;"
  done
  for ip in $(curl -s 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 and apply the configuration:

bash
sudo nginx -t && sudo systemctl reload nginx

After this, $remote_addr in nginx's configuration — including the standard log_format — automatically becomes the visitor's real IP instead of Cloudflare's; there's no need to change log_format separately.

Apache: the mod_remoteip module

On Ubuntu/Debian the module already ships with the apache2 package — you just need to enable it:

bash
{
  echo "RemoteIPHeader CF-Connecting-IP"
  for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do
    echo "RemoteIPTrustedProxy $ip"
  done
} | sudo tee /etc/apache2/conf-available/remoteip.conf > /dev/null
bash
sudo a2enmod remoteip
sudo a2enconf remoteip
sudo apache2ctl configtest
sudo systemctl restart apache2

By default Apache logs the IP via %h. To get the real IP in the logs too, replace %h with %a in LogFormat (in /etc/apache2/apache2.conf).

Tip: on AlmaLinux/Rocky the same module usually ships with the httpd package too, but may not be loaded — check with httpd -M | grep remoteip. If it's missing from the list, add LoadModule remoteip_module modules/mod_remoteip.so in a file under /etc/httpd/conf.modules.d/, and put the same RemoteIPHeader/RemoteIPTrustedProxy directives in /etc/httpd/conf.d/remoteip.conf — the same pattern as other modules on this distro family, see "Apache: install and virtual hosts".
Important: Cloudflare's IP ranges change occasionally — the current list always lives at https://www.cloudflare.com/ips-v4 and https://www.cloudflare.com/ips-v6. It's worth re-running the config-generation commands from time to time, for example on a cron schedule (see "Cron and backup automation" for cron syntax), so the list doesn't go stale.
Important: only trust the CF-Connecting-IP header on requests that genuinely came through Cloudflare. If the origin is still reachable directly on its real IP, bypassing Cloudflare, anyone can set that header to whatever they like. The next step closes that gap with the firewall.

Step 7. Restrict 80/443 to Cloudflare's networks only (optional)

A Proxied record hides the VPS's IP from a casual DNS lookup, but if the real IP has leaked some other way (an old DNS cache, a DNS only record left behind by accident, a scan of your host's IP ranges), Cloudflare's protection — including basic DDoS smoothing — simply doesn't apply: an attacker can reach the server directly, bypassing Cloudflare entirely. To close that gap, allow 80/443 only from Cloudflare's networks.

The approach below is for ufw (Ubuntu/Debian). If ufw isn't set up yet, go through "The ufw firewall" first; like that article itself, this step doesn't cover firewalld on AlmaLinux/Rocky — you can get the same result there with rich rules (firewall-cmd --add-rich-rule), but that's a separate topic with its own syntax. Then add allow rules for Cloudflare's ranges:

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

Now remove the old broad rules that opened 80/443 to everyone (80/tcp, 443/tcp, Nginx Full, and similar) — check their numbers and delete them one at a time, starting from the highest number:

bash
sudo ufw status numbered
sudo ufw delete <RULE_NUMBER>
Important: before deleting the broad rules for 80/443, make sure the SSH rule (usually 22/tcp) is definitely there and untouched — otherwise you risk losing access not just to the site, but to the server itself. For more on working with rule numbers, see "The ufw firewall".

Repeat this step together with updating the realip configuration from step 6 — they both use the same ranges.

What's next

The domain now runs through Cloudflare: the CDN speeds up static delivery worldwide, the VPS's IP is hidden from regular visitors and scanners, and basic volumetric DDoS attacks get absorbed at Cloudflare's edge before they ever reach the server. Related reading: "DNS records: A, AAAA, CNAME, and more" and "How to point a domain at your server" if you need a refresher on how records work without Cloudflare; "The ufw firewall" and "Server security checklist" to secure the VPS itself, not just hide it behind a CDN.