How to Protect a VPS from DDoS Attacks: A Step-by-Step Guide with Cloudflare
A DDoS attack floods a server with requests or traffic until the site stops responding. The free Cloudflare plan, combined with a properly configured VPS, stops most attacks aimed at websites.
This guide covers seven steps: connect Cloudflare, hide the real IP, block direct access to the server, and harden Nginx and the Linux kernel. Commands are for Ubuntu/Debian with Nginx.
There are two types of attacks, and each needs a different defense:
- L3/L4 (network) attacks saturate the bandwidth or the TCP stack, such as SYN floods and UDP floods. Defense: Cloudflare with a hidden origin IP, otherwise filtering at the hosting provider.
- L7 (application) attacks imitate visitors and overload the application, such as HTTP floods or search page abuse. Defense: Cloudflare WAF and rate limiting, plus Nginx limits.
Step 1. Connect Your Domain to Cloudflare
Once connected, all site traffic passes through Cloudflare's network, so an attack hits Cloudflare instead of your VPS.
- Sign up at cloudflare.com and click Add a domain. Choose the Free plan.
- Cloudflare imports your DNS records. Compare them with the current ones at your registrar: A, AAAA, CNAME, MX, TXT.
- Turn on the orange cloud (Proxied) for your site records (
@,www). A grey cloud (DNS only) does not protect traffic. - At your domain registrar, replace the nameservers with the two Cloudflare provided.
- Wait for the Active status. This usually takes 15 minutes to a few hours.
Check: dig +short example.com should return Cloudflare IPs, not your server's IP.
Step 2. Configure Cloudflare Security
Cloudflare's basic DDoS protection is always on, but without WAF rules and rate limits some L7 attacks still reach the server.
SSL/TLS
- Set the encryption mode to Full (strict). Flexible mode leaves traffic to your server unencrypted.
- Under Origin Server, create an Origin Certificate (valid up to 15 years) and install it in Nginx.
- Turn on Always Use HTTPS.
Security → Settings
- Bot Fight Mode: on. It blocks simple bots.
- Security Level: Medium. Switch to High during an attack.
- Browser Integrity Check: on.
WAF → Custom rules (5 rules on the Free plan). Example expressions:
- Protect the admin login (action: Managed Challenge):
(http.request.uri.path contains "/wp-login.php") or (http.request.uri.path contains "/wp-admin") - Challenge countries outside your audience (Managed Challenge):
(not ip.src.country in {"US" "GB" "DE"}) - Block XML-RPC (Block):
(http.request.uri.path eq "/xmlrpc.php")
WAF → Rate limiting rules (1 rule on the Free plan). Example: more than 100 requests to / from one IP in 10 seconds → Block for 10 seconds. Tune the threshold using your analytics so regular visitors are not affected.
Caching: the more pages Cloudflare serves from cache, the fewer requests reach your VPS. Cache static assets and set up Cache Rules for pages that are the same for every visitor.
Step 3. Hide Your Server's Real IP
If an attacker knows your VPS IP, they can hit it directly and bypass Cloudflare. This is the most common reason protection "doesn't work".
Common IP leaks:
- Old DNS records. DNS history services store the IP your domain pointed to before Cloudflare. If the site used to run without the proxy, ask your host for a new IP.
- Grey-cloud subdomains.
mail,ftp,cpanelanddevoften point to the same server. Proxy them or move them to another IP. - Email. MX records and email headers reveal the sender's IP. Send mail through an external SMTP relay or a separate server.
- Outgoing requests. Webhooks, WordPress pingbacks and fetching images by URL expose your server's IP to other sites.
- Responding on the bare IP. Scanners crawl the whole internet and find sites by IP and SSL certificate. Steps 4 and 5 show how to close this.
Check: curl -I http://SERVER_IP from another machine should not return your site.
Step 4. Allow Ports 80/443 Only from Cloudflare
Your site's ports should accept connections only from Cloudflare addresses. Then a bot that finds your IP can't reach Nginx.
Allow SSH first, or you will lock yourself out. Ideally, only from your own IP:
ufw allow from YOUR_IP to any port 22 proto tcp
ufw default deny incomingufw default allow outgoing
Then open 80 and 443 to Cloudflare's ranges. The current lists are at https://www.cloudflare.com/ips-v4 and https://www.cloudflare.com/ips-v6:
for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do
ufw allow proto tcp from $ip to any port 80,443 comment 'Cloudflare'
done
ufw enableufw status numbered
Important:
- Cloudflare's ranges change occasionally. Check them monthly, for example with a cron script.
- Docker publishes ports bypassing UFW. If your site runs in a container, bind the port to
127.0.0.1and proxy it through Nginx on the host. - For stricter protection, enable Authenticated Origin Pulls in Cloudflare. Nginx will then accept only requests carrying Cloudflare's client certificate.
Step 5. Configure Nginx
Nginx is the last line of defense against L7 attacks that get past Cloudflare. Configure three things: the visitor's real IP, limits, and a catch-all for requests to the bare IP.
Visitor's real IP. Without it, Nginx sees only Cloudflare addresses and per-IP limits are useless. Generate the config:
{ for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do echo "set_real_ip_from $ip;" done echo "real_ip_header CF-Connecting-IP;"} > /etc/nginx/conf.d/cloudflare-realip.conf
Request and connection limits. In the http block (/etc/nginx/nginx.conf):
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s;limit_conn_zone $binary_remote_addr zone=conn_limit:10m;limit_req_status 429;limit_conn_status 429;client_header_timeout 10s;client_body_timeout 10s;send_timeout 10s;keepalive_timeout 15s;client_max_body_size 10m;
In your site's server block:
limit_req zone=req_limit burst=20 nodelay;limit_conn conn_limit 20;
10 requests per second with a burst of 20 suits a typical site. For an API or SPA with frequent requests, raise rate and burst.
Catch-all for bare-IP requests. The default server drops the connection if a request is not for your domain:
server { listen 80 default_server; listen 443 ssl default_server; ssl_reject_handshake on; # Nginx 1.19.4+ server_name _; return 444;}
Test and apply: nginx -t && systemctl reload nginx.
Step 6. Harden the Kernel and Set Up fail2ban
Kernel network settings. These protect against SYN floods and address spoofing. Create /etc/sysctl.d/99-ddos.conf:
net.ipv4.tcp_syncookies = 1net.ipv4.tcp_max_syn_backlog = 4096net.ipv4.tcp_synack_retries = 2net.ipv4.tcp_fin_timeout = 15net.core.somaxconn = 4096net.ipv4.conf.all.rp_filter = 1net.ipv4.conf.default.rp_filter = 1net.ipv4.icmp_echo_ignore_broadcasts = 1net.ipv4.conf.all.accept_redirects = 0net.ipv4.conf.all.send_redirects = 0
Apply: sysctl --system.
fail2ban. One catch: behind Cloudflare, all packets arrive from Cloudflare IPs. An iptables ban on the server won't stop the attacker. For the website, use the cloudflare-token action instead: it blocks the IP at Cloudflare via the API.
apt install fail2ban
Create a Cloudflare API token with Zone → Firewall Services → Edit permission and add this to /etc/fail2ban/jail.local:
[sshd]enabled = truemaxretry = 5bantime = 1h[nginx-limit-req]enabled = truefilter = nginx-limit-reqlogpath = /var/log/nginx/error.logfindtime = 60maxretry = 10bantime = 1haction = cloudflare-token[cfzone="ZONE_ID", cftoken="API_TOKEN"]
Restart with systemctl restart fail2ban and check with fail2ban-client status nginx-limit-req. SSH keeps the regular iptables ban, since it doesn't go through Cloudflare.
Step 7. What to Do During an Attack
First, turn on Under Attack Mode. Decide the rest based on what you find.
- Turn on Under Attack Mode (Overview → Quick Actions). Every visitor passes a JavaScript check, and bots are filtered out. Use it only during the attack: it gets in the way of search crawlers and APIs.
- Find where the traffic is going. Check Security → Events and Analytics in Cloudflare. A spike there means the attack goes through Cloudflare. If it's quiet there but the server is down, your IP is being hit directly.
- Check the load on the server:
# connections by state: many SYN-RECV = SYN floodss -ant | awk 'NR>1 {print $1}' | sort | uniq -c | sort -nr
# top IPs by requests in the Nginx logawk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -20
# top URLs: what exactly is being attackedawk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -20 - Block the source in Cloudflare. Add a custom rule by path, User-Agent, country or ASN from the logs. For suspicious ASNs, use Managed Challenge rather than Block.
- Take load off the application. Cache the targeted pages with Cache Rules and temporarily disable heavy search and filters.
- If your IP is attacked directly, Cloudflare can't help. Contact your hosting support: you need network-level filtering or a new IP. After the change, check for leaks from Step 3.
Where Cloudflare Won't Help
The free Cloudflare plan protects only HTTP/HTTPS web traffic. Everything else on the server stays exposed.
- Game servers, VPNs, other TCP/UDP services. The Free plan proxies only HTTP/HTTPS. You need network-level protection from your host or the paid Cloudflare Spectrum.
- Your server's IP is already known. The attack goes straight to the server, bypassing Cloudflare. You need a new IP and filtering at the host.
- Volumetric attack on the IP. The link is saturated before traffic ever reaches the VPS firewall. Only filtering before the server helps.
- Email, SSH, control panels. These aren't proxied. Move them to a separate IP and restrict access with an allowlist.
Settings inside the VPS help against L7 attacks and small floods. Only filtering at the data center stops an attack of tens of gigabits that saturates the link.
Checklist
FAQ
Is the free Cloudflare plan enough for DDoS protection? For most websites, yes, as long as the real IP is hidden and WAF rules and limits are set. It's not enough for non-HTTP services or if your IP has leaked.
Will Cloudflare slow down my site? Usually not. Caching and the CDN more often make pages load faster. Only Under Attack Mode adds a delay of a few seconds for the browser check.
Can I protect a game server with Cloudflare? Not on the free plan: it proxies only HTTP/HTTPS. You need network-level protection from your host or Cloudflare Spectrum.
How do I know my server is under a DDoS attack? The site slows down or goes offline, CPU load and connection counts spike, and the logs fill with near-identical requests. See Step 7 for diagnostic commands.
Why does my server still go down after connecting Cloudflare? Most likely your IP is being attacked directly. Check for leaks in Step 3 and close the ports as in Step 4.