Infrastructure

Locking Down a VPS: A Pragmatic Security Setup

How to secure a Linux VPS by hiding it from the public internet: restricting administration to Tailscale and routing web traffic through Cloudflare Tunnels.

A

Azhar

Author

June 27, 2026

Published

8 min read

Read time

Locking Down a VPS: A Pragmatic Security Setup

1 Securing the Origin Server: Restricting Web Traffic to Cloudflare Only

To maximize security for our web infrastructure while maintaining seamless administrative access, we implemented a strict firewall policy using UFW (Uncomplicated Firewall) on our Ubuntu origin server. The goal was simple: ensure that our web applications are only accessible via Cloudflare’s proxy network, effectively hiding our real server IP from the public internet and mitigating direct attacks (DDoS, brute-force, or origin-exposure exploits).

The Logic Behind the Rules

We adopted a “default deny” stance for all incoming connections while keeping outgoing traffic unrestricted for updates and external API calls. Here is the exact breakdown of our policy:

  • SSH (Port 22): Allowed from anywhere. This guarantees we never lose remote administrative access, regardless of our current location or network changes.
  • Web Ports (80, 443, 8080): Restricted exclusively to the official Cloudflare IP ranges (both IPv4 and IPv6). By doing this, we force all client traffic to route through Cloudflare’s edge. If someone discovers our origin IP and tries to connect directly, the firewall silently drops their packets.
  • Outgoing Traffic: Set to allow by default, enabling the server to freely fetch OS updates, connect to external databases, and send responses back to established Cloudflare connections.

Technical Implementation

We started by resetting UFW and setting the global policies:

sudo ufw default deny incoming
sudo ufw default allow outgoing

For SSH, we kept the standard open rule:

sudo ufw allow 22/tcp

Rather than manually adding fifteen IPv4 blocks and seven IPv6 blocks across three separate ports, we streamlined the process using a bash loop. This bulk approach significantly reduced the chance of typos and kept the rule-set clean:

# Add all Cloudflare IPv4 ranges for ports 80, 443, and 8080
for ip in 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22; do
    sudo ufw allow from $ip to any port 80,443,8080
done

# Add all Cloudflare IPv6 ranges
for ip in 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32; do
    sudo ufw allow from $ip to any port 80,443,8080
done

Finally, we enabled the firewall and verified the active rules:

sudo ufw enable
sudo ufw status verbose

Important Considerations

Stateful Tracking: Because UFW is stateful, we do not need to worry about outbound reply traffic. Once a Cloudflare connection is permitted in, the server automatically allows the response packets to travel back out.

Cloud Provider Firewalls (AWS/GCP/Azure): If you are running on a cloud VM, remember that your provider’s external Security Groups act as a separate perimeter. You must also whitelist these Cloudflare ranges there; otherwise, the cloud firewall may drop traffic before it reaches UFW.

The result is a hardened origin server that remains invisible to scanners, only responding to legitimate requests proxied through Cloudflare, while keeping SSH reliably accessible for our engineering team.

2 Private Mesh Networking with Tailscale: Closing SSH to the World

If you’re still leaving port 22 open to the entire internet, you’re essentially begging for brute-force bots to hammer your server logs. Sure, you can configure tools like fail2ban (which I do), but why even give them the chance to knock on the door?

I decided to close port 22 to the public internet entirely and route all my administrative traffic through a private Tailscale mesh network.

Why Tailscale?

Tailscale builds a secure, zero-config mesh VPN using WireGuard. It assigns each of my devices a private IP (in the 100.x.y.z range) that only speaks to other authenticated devices on my “tailnet.”

This means my laptop can SSH directly into my VPS as if it were sitting on my local desk, but to the rest of the world, port 22 might as well not exist.

Step-by-Step Lockdown

First, install Tailscale on the VPS. It’s a single command:

curl -fsSL https://tailscale.com/install.sh | sh

Once installed, authenticate the machine:

sudo tailscale up

This spits out a login link. Click it, authenticate with your provider, and your VPS is now a node on your private network.

Now, we tell our firewall (UFW) to allow traffic over the Tailscale interface (tailscale0). The beauty of UFW is we can allow the entire interface:

sudo ufw allow in on tailscale0

With that rule in place, we can safely delete the global SSH rule we created in Step 1. If we don’t, port 22 is still wide open.

# Find the rule number for "22/tcp"
sudo ufw status numbered

# Delete the rule (usually rule 1 or 2)
sudo ufw delete <rule-number>

(A quick warning: Make sure you are connected to your Tailscale network on your local machine BEFORE you delete the global SSH rule, or you will lock yourself out of your server. Ask me how I know.)

Now, test the connection from your local terminal:

ssh user@your-vps-tailscale-ip

If it connects, your server’s administration portal is now completely invisible to the public internet.

3 Cloudflare Tunnel for Web Traffic: The No-Inbound-Ports Holy Grail

In Step 1, we spent all that effort whitelisting Cloudflare’s IP ranges so only their proxy could reach our web ports (80, 443, 8080). It’s a solid defense. But what if we could close those ports too?

Enter Cloudflare Tunnels (cloudflared).

Why is this needed?

With a traditional setup, your VPS must listen on a public IP address and wait for inbound connections. Even if UFW blocks everything except Cloudflare, your server is still listening. A misconfigured firewall rule or a bypass could expose your origin.

Cloudflare Tunnel flips this model. Instead of waiting for connections, the server runs a small daemon (cloudflared) that establishes an outbound connection to Cloudflare’s nearest edge datacenter.

The advantages are massive:

  • No inbound ports needed: We can block ports 80, 443, and 8080 completely.
  • NAT Traversal: It works even if your cheap VPS is behind a strict CGNAT (common with ultra-budget hosts or home servers) with no public IPv4.
  • Auto-HTTPS: Cloudflare handles the SSL certificates at the edge. No certbot, no cron jobs for renewals.

Setting Up the Tunnel

While you can configure tunnels via CLI config files, Cloudflare’s dashboard has made this ridiculously simple.

  1. Go to the Cloudflare Zero Trust dashboard.
  2. Navigate to Networks -> Tunnels and click Create a Tunnel.
  3. Select Cloudflare Tunnel (connector), name it, and copy the installation command for your OS (Ubuntu/Debian in our case).

It looks something like this:

curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb && \
sudo dpkg -i cloudflared.deb && \
sudo cloudflared service install <your-tunnel-token>

This downloads cloudflared, installs it, and registers it as a systemd service that starts automatically on boot.

Once the service is running, you go back to the dashboard and configure the routing:

  • Public Hostname: yourdomain.com
  • Service: http://localhost:8080 (or whatever port your local web application is running on, like a Spring Boot app or a Docker container).

The Ultimate Lockdown

Now that the tunnel is running, all traffic to yourdomain.com is routed through the outbound tunnel directly to your local application port.

This means we can go back to UFW and completely delete the rules we wrote in Step 1 for ports 80 and 443. We don’t need them.

# Reset UFW and keep only Tailscale allowed
sudo ufw reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on tailscale0
sudo ufw enable

If you run sudo ufw status verbose now, you’ll see a beautiful sight: the only incoming traffic allowed is over your private Tailscale network. Your web application is serving traffic to the world, yet your server doesn’t have a single open port on the public internet.


Honest Reflections

Does this setup have downsides? Of course.

If Cloudflare goes down (unlikely, but it happens), or if your Tailscale daemon crashes, you are in for a bad day. You’re trading standard, decentralized protocols for convenience and security tied to these platforms.

But for a single-developer setup or a small team running on a budget VPS? The security-to-effort ratio of this stack is unbeatable. I no longer check my auth logs to see script kiddies trying to log in as root. They can’t even find the door.

A

Azhar

Published on June 27, 2026