In Part 1, we locked down user access and SSH. In Part 2, we layered UFW and DigitalOcean's cloud firewall on top, and deployed Fail2Ban.

Your server is hardened. But port 22 is still open. Every SSH port scanner on the internet can find it. Fail2Ban handles the brute-force attempts, but the surface still exists.

This article removes it entirely.

This part is optional. Parts 1 and 2 already give you a genuinely hardened server. This article goes further by eliminating all open ports using Cloudflare Tunnel and Cloudflare Access, which means you'll need a Cloudflare account and a domain managed through Cloudflare (the free plan works). If you don't use Cloudflare, your server is still well-protected with what you've built so far.


What Is Cloudflare Tunnel?

Cloudflare Tunnel (cloudflared) creates an outbound-only encrypted connection from your server to Cloudflare's network. Your server initiates the connection — no inbound ports required.

Once the tunnel is up:

  • Traffic flows: your browser → Cloudflare → tunnel → your server
  • Your server has zero open inbound ports
  • SSH works through the tunnel, not through an exposed port 22
  • Web traffic routes through Cloudflare's CDN and DDoS protection automatically

The attack surface shrinks from "any IP on the internet can knock on port 22" to "only authenticated identities approved through Cloudflare Access can reach the server."


Prerequisites

  • A domain on Cloudflare (free plan works)
  • A Cloudflare account
  • The server from Parts 1 and 2

1. Install cloudflared

On your server:

# Add Cloudflare's package repository
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb

# Verify installation
cloudflared --version

2. Authenticate cloudflared

cloudflared tunnel login

This prints a URL in your terminal. Since you're on a headless server, copy that URL and open it in your local machine's browser. Select the domain you want to use and authorize. Cloudflare will then download a credentials certificate to ~/.cloudflared/cert.pem on your server automatically.


3. Create a Tunnel

cloudflared tunnel create my-server

This creates a tunnel with a UUID and stores credentials in ~/.cloudflared/<UUID>.json. Note the UUID — you'll use it in config.

Verify it exists:

cloudflared tunnel list

4. Configure the Tunnel

Create the config file. If you authenticated as root, the credentials live under /root/.cloudflared/. If you authenticated as your sudo user, they're under /home/myusername/.cloudflared/. Use the path that matches:

# If authenticated as root:
sudo nano /root/.cloudflared/config.yml

# If authenticated as your sudo user:
nano ~/.cloudflared/config.yml

Add the following (replace values with your own):

tunnel: <YOUR-TUNNEL-UUID>
credentials-file: /home/myusername/.cloudflared/<YOUR-TUNNEL-UUID>.json

ingress:
  # Route your domain to your web server
  - hostname: yourdomain.com
    service: http://localhost:80

  - hostname: www.yourdomain.com
    service: http://localhost:80

  # SSH access through the tunnel
  - hostname: ssh.yourdomain.com
    service: ssh://localhost:22

  # Catch-all — return 404 for anything not matched
  - service: http_status:404

Note on credentials-file: Use the path that matches where you ran cloudflared tunnel create. If you ran it as root, use /root/.cloudflared/. If as your sudo user, use /home/myusername/.cloudflared/. This path will be updated in the next section when we install the system service.

What This Does

  • yourdomain.com and www.yourdomain.com → proxied to your local nginx/app on port 80, with Cloudflare handling HTTPS termination
  • ssh.yourdomain.com → SSH tunneled through Cloudflare to local port 22
  • Everything else → 404

5. Create DNS Records

# Point your domain to the tunnel
cloudflared tunnel route dns my-server yourdomain.com
cloudflared tunnel route dns my-server www.yourdomain.com
cloudflared tunnel route dns my-server ssh.yourdomain.com

This creates CNAME records in Cloudflare DNS pointing each hostname to your tunnel's <UUID>.cfargotunnel.com address. Traffic never touches your server's IP directly.


6. Run the Tunnel as a System Service

We want the tunnel to start automatically and restart on failure.

# Install as a systemd service
sudo cloudflared service install

cloudflared service install copies your config and credentials to /etc/cloudflared/. After this, the service reads from /etc/cloudflared/config.yml, not from your home directory.

Verify the credentials path in the installed config is correct:

sudo cat /etc/cloudflared/config.yml

The credentials-file should point to /etc/cloudflared/<YOUR-TUNNEL-UUID>.json. If it still points to your home directory, update it:

# Copy the credentials file if it wasn't copied automatically
sudo cp ~/.cloudflared/<YOUR-TUNNEL-UUID>.json /etc/cloudflared/

# Update the path in config
sudo nano /etc/cloudflared/config.yml
# Change credentials-file to: /etc/cloudflared/<YOUR-TUNNEL-UUID>.json

Now enable and start:

sudo systemctl enable cloudflared
sudo systemctl start cloudflared

# Verify it's running
sudo systemctl status cloudflared

Check the logs:

sudo journalctl -u cloudflared -f

You should see the tunnel connecting to Cloudflare's network. If you see permission errors about the credentials file, double-check that it exists at the path specified in /etc/cloudflared/config.yml.

Once active, your web traffic is flowing through the tunnel.


7. Close Port 22

This is the step that matters most. With the tunnel running, SSH is accessible through ssh.yourdomain.com — port 22 no longer needs to be open.

Remove the UFW Rule

In Part 2, we used ufw limit ssh (not allow ssh). The delete command must match the rule type:

# If you used 'limit' in Part 2 (recommended):
sudo ufw delete limit ssh

# If you used 'allow' instead:
sudo ufw delete allow ssh

# Verify
sudo ufw status verbose

Common mistake: Running sudo ufw delete allow ssh when the actual rule is limit ssh does nothing. UFW won't warn you. Port 22 stays open and you think it's closed. Always check ufw status after deleting to confirm the rule is actually gone.

Port 22 should no longer appear in the allowed list.

Remove SSH from the Cloud Firewall

In the DigitalOcean control panel, go to your cloud firewall and delete the SSH inbound rule.

Verify Port 22 Is Closed

From an external machine:

nmap -p 22 your_server_ip

Expected result:

PORT   STATE    SERVICE
22/tcp filtered ssh

filtered means the packet was dropped. Port 22 is unreachable from the internet.

Recovery note: With port 22 closed, if your Cloudflare Tunnel goes down, you cannot SSH in. DigitalOcean provides a web-based console (Droplet → Access → Launch Droplet Console) that bypasses the network entirely. Bookmark it. That's your emergency hatch if the tunnel breaks and you need to fix the cloudflared service.


8. SSH Through the Tunnel

Your server's SSH port is closed to the internet, but you can still SSH into it through Cloudflare.

Install cloudflared Locally

On your local machine:

# macOS (recommended — works for both Intel and Apple Silicon)
brew install cloudflare/cloudflare/cloudflared

# Or download directly:
# Apple Silicon (M1/M2/M3/M4):
curl -L --output cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz
tar -xzf cloudflared-darwin-arm64.tgz
sudo mv cloudflared /usr/local/bin/

# Intel Mac:
curl -L --output cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz
tar -xzf cloudflared-darwin-amd64.tgz
sudo mv cloudflared /usr/local/bin/

Configure SSH to Use the Tunnel

Edit your local ~/.ssh/config:

Host ssh.yourdomain.com
  HostName ssh.yourdomain.com
  User myusername
  IdentityFile ~/.ssh/id_ed25519
  ProxyCommand cloudflared access ssh --hostname %h

Connect

ssh ssh.yourdomain.com

What happens:

  1. The SSH client hits the ProxyCommand
  2. cloudflared access ssh authenticates with Cloudflare
  3. The connection tunnels through Cloudflare to your server's local SSH daemon
  4. You're in — without port 22 being open anywhere

9. Cloudflare Access: Zero Trust Authentication

Right now, anyone who installs cloudflared locally can attempt to connect via the tunnel. Let's restrict that with Cloudflare Access — which sits in front of ssh.yourdomain.com and requires authentication before a connection is allowed.

Create an Access Application

  1. In the Cloudflare dashboard, go to Zero Trust → Access → Applications
  2. Click Add an application → Self-hosted
  3. Configure:
    • Application name: My Server SSH
    • Session duration: 24h (or whatever fits your workflow)
    • Application domain: ssh.yourdomain.com

Create an Access Policy

  1. Add a policy:

    • Policy name: Allow Me
    • Action: Allow
    • Include rule: Emails → add your email address
  2. Save the application.

Now when anyone (including you) tries to SSH through the tunnel, Cloudflare Access intercepts and requires authentication with your email. You get a one-time code sent to your inbox. After authentication, the SSH session proceeds.

An attacker would need:

  1. Your email address
  2. Access to your inbox
  3. Your SSH private key

All three. That's the Zero Trust model in practice.

Optional: Restrict to a Specific IP Range

If you work from a fixed IP or office network, add a second condition to the policy:

  • Include rule: IP Ranges → add your trusted CIDRs

Now access requires the right email and the right IP. One-time codes from unknown IPs are rejected before they're sent.


10. Verify the Full Setup

Check the Tunnel

cloudflared tunnel info my-server

Check Web Traffic

Visit https://yourdomain.com in a browser. It should load through Cloudflare. Check the headers — you'll see CF-Ray headers confirming Cloudflare proxied the request.

Confirm No Open Ports

# From your server — what's listening
sudo ss -tlnp

# From outside — what's reachable
nmap -sV your_server_ip

The external scan should show all ports as filtered. Nothing open. Nothing to attack.


The Complete Architecture

Here's what we've built across all three parts:

Internet
    │
    ▼
Cloudflare Network
    │  ← DDoS protection, WAF, Access authentication
    │
Cloudflare Tunnel (outbound only, encrypted)
    │
    ▼
Your Server
    │
    ├── UFW (deny all ingress)
    ├── Fail2Ban (ban repeated failures)
    ├── nginx / app (bound to localhost only for internal services)
    └── sshd (accessible only via tunnel — no open port)

Traffic enters through Cloudflare's network. The tunnel carries it to the server. UFW and Fail2Ban cover anything that somehow bypasses the tunnel layer. SSH requires Cloudflare Access authentication before a connection is allowed through.


Security Posture: Final State

  • ✅ Zero open inbound ports on the server
  • ✅ SSH access through Cloudflare Tunnel — requires email auth + SSH key
  • ✅ Web traffic through Cloudflare CDN with DDoS protection
  • ✅ UFW denies all ingress at the host level
  • ✅ DigitalOcean cloud firewall as a network-level backstop
  • ✅ Fail2Ban banning repeated failures on any service that's exposed
  • ✅ Application-specific users limiting blast radius of any compromise
  • ✅ Password auth and root login permanently disabled

What This Cost

Cloudflare Tunnel is free. Cloudflare Access is free for up to 50 users. The DigitalOcean cloud firewall is free. UFW and Fail2Ban are free and ship with Ubuntu.

The entire stack — host firewall, cloud firewall, tunnel, Zero Trust access — costs nothing except an afternoon of setup.

Most servers on the internet have none of this. Yours now has all of it.

That's the series. Three articles, one genuinely hardened server. 🔒