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.
Step 1. Add the domain to Cloudflare and switch nameservers
- Sign up or log in at dash.cloudflare.com.
- Click Add a domain (on some dashboard versions, Onboard a domain) and enter the domain without
wwwor a protocol, e.g.example.com. - Pick a plan — Free is enough for everything covered in this article.
- Cloudflare scans the domain's current DNS records and offers to import them. Review the list carefully — especially mail records (
MX,TXTwith 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. - Cloudflare assigns two nameservers like
xxxx.ns.cloudflare.com— go to your domain registrar's panel and replace the current nameservers with those two.
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:
dig NS example.com +shortOnce 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.
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-styleTXT, autodiscover/autoconfigCNAME) as DNS only: Cloudflare doesn't proxy mail traffic, and a proxiedCNAMEcan 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.
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.
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
certbotas 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-cloudflareplugin. 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):
sudo snap install certbot-dns-cloudflare
sudo snap set certbot trust-plugin-with-root=ok
sudo snap connect certbot:plugin certbot-dns-cloudflareCheck that the plugin connected:
certbot pluginsdns-cloudflare should show up in the list. Save the token to a file readable only by root:
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.iniReplace <CLOUDFLARE_API_TOKEN> with the real token from the Cloudflare dashboard, then request the certificate:
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
--dns-cloudflare-propagation-seconds 60 \
-d example.com -d www.example.comcertonly 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.
Check the result after either approach:
curl -I https://example.com/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 equalsexample.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.
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:
{
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/nullCheck and apply the configuration:
sudo nginx -t && sudo systemctl reload nginxAfter 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:
{
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/nullsudo a2enmod remoteip
sudo a2enconf remoteip
sudo apache2ctl configtest
sudo systemctl restart apache2By 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).
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:
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
doneNow 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:
sudo ufw status numbered
sudo ufw delete <RULE_NUMBER>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.