What the cloud firewall does differently
A cloud firewall operates at the network edge of your provider's infrastructure. It processes packets before they reach your virtual machine. The key behaviors:
- Stateless filtering — matches source IP against your allowlist and drops everything else. No connection tracking, no state tables to exhaust
- Silent drop — a DROP rule sends no response. No RST packet, no ICMP unreachable, nothing. To the scanner, your IP appears to not exist. The connection times out after the scanner's default timeout — typically 30 to 60 seconds — during which it can't move on to the next target
- No server resource consumption — nginx never sees the packet. No worker connection, no log entry, no CPU cycle spent
A cloud firewall is not an application-layer filter. It can't inspect HTTP headers, block specific URIs, or evaluate request methods. Those jobs belong to Cloudflare at the edge and nginx at the origin. The cloud firewall does exactly one thing: it answers the question "is this IP allowed to talk to my server at all?"
Step 1: Cloud firewall rules
Linode's (Akami) Cloud Firewall is free — no additional cost beyond the instance itself. Vultr, DigitalOcean, and Hetzner all include firewall services at no charge. For a zero-cost layer that drops unwanted traffic before it touches your server's kernel, there's no reason not to use it.
Create a cloud firewall and attach it to your instance. The rules below use the Linode Cloud Firewall interface as a reference, but the logic translates to any provider — inbound rules, outbound rules, and a default policy.
Inbound rules
Protocol: TCP
Ports:
22 (or your custom SSH port)Sources: Any IPv4/IPv6 (or restrict to home/office)
Action: Accept
SSH stays open to all networks. You need to reach this server from anywhere — home, office, hotel WiFi, VPN exit node.
fail2ban on the host handles brute-force mitigation, not the firewall. If you only ever SSH from a static home or office IP, restrict the sources to that address — it's a free second layer. The Hardening SSH guide covers both approaches.
Protocol: TCP
Ports:
80Sources: Cloudflare's IPv4 and IPv6 ranges only
Action: Accept
Cloudflare's official IP ranges are published at cloudflare.com/ips. Add every range — 15 IPv4 CIDR blocks and 7 IPv6 blocks as of publication.
Protocol: TCP
Ports:
443Sources: Cloudflare's IPv4 and IPv6 ranges only
Action: Accept
Same IP ranges as port 80. Cloudflare connects to your origin over HTTPS in Full (Strict) mode — this rule is the only path for web traffic.
Action: DROP
Any packet that doesn't match one of the three rules above is silently discarded. No response, no timeout negotiation, no indication that a server exists at this address.
Outbound rules
Outbound traffic typically uses the default ACCEPT policy — your server can initiate connections to any destination. If you want to lock this down further, restrict outbound to ports 80 and 443 for package updates and API calls, plus your custom SSH port for administrative use. The default ACCEPT policy is reasonable for most setups. For monitoring outbound connections from PHP — catching compromised plugins that phone home — the Squid proxy guide covers forcing all PHP-FPM traffic through a local proxy with domain blocklists and connection auditing.
Once applied, test immediately from an external connection. If you locked yourself out of SSH, your provider's web console (Lish, VNC, serial console) still works — the cloud firewall operates outside the VM.
Step 2: Nginx loopback for internal processes
Your server's own cron jobs, health checks, and renewal scripts make HTTP requests to itself. If those requests resolve to your public IP, the cloud firewall drops them — they don't originate from a Cloudflare IP. The fix is to ensure internal requests use the loopback interface.
Your HTTPS server blocks already listen on the standard ports. The HTTP catch-all redirect needs to explicitly bind to loopback so internal processes can reach it without leaving the server:
# HTTP-to-HTTPS redirect — binds loopback for internal processes
server {
listen 127.0.0.1:80;
listen [::1]:80;
listen 80;
listen [::]:80;
server_name _;
return 301 https://$host$request_uri;
}
The listen 127.0.0.1:80 and listen [::1]:80 directives ensure that requests originating from the server itself — certbot renewal validations, wp-cron, monitoring scripts — hit the redirect on the loopback interface. Those packets never touch the cloud firewall. The standard listen 80 and listen [::]:80 directives handle external traffic that arrives via Cloudflare.
Step 3: The certificate renewal gotcha
Let's Encrypt validates domain ownership by making an HTTP request to a specific path on your server. The validation servers use their own IP addresses — not Cloudflare's. If your cloud firewall drops all non-Cloudflare traffic on ports 80 and 443, certificate renewal fails.
The safeguard is Cloudflare's proxy. When every domain on your certificate is proxied (orange cloud), Let's Encrypt resolves the domain to Cloudflare's IPs, Cloudflare forwards the request to your origin, and the request arrives from a Cloudflare IP — passing the firewall check. The validation succeeds.
If any subdomain on a multi-domain certificate is DNS-only (grey cloud), renewal breaks. Example: you have a certificate covering example.com, www.example.com, and ssh.example.com. The first two are proxied. ssh.example.com points directly to your server for SSH hostname resolution — it's grey-clouded. During renewal, Let's Encrypt tries to validate ssh.example.com and hits your origin IP directly. The cloud firewall drops the request. Renewal fails across the entire certificate.
The fix: keep DNS-only subdomains on a separate certificate, or remove them from the multi-domain certificate entirely:
# Remove an unproxied subdomain from the certificate
certbot certonly --webroot -w /var/www/html \
-d example.com -d www.example.com -d blog.example.com
After the change, verify every domain on the certificate is proxied through Cloudflare. Run a renewal dry-run to confirm:
certbot renew --dry-run
If the dry-run succeeds, automatic renewal will work. If it fails, check which domain caused the failure and verify its Cloudflare DNS record is set to proxied (orange cloud). The full Certbot configuration — webroot authentication, renewal hooks, and shared webroot directories — is covered in the Let's Encrypt guide.