Eliminate Your SSH Attack Surface with Cloudflare Tunnel
I got tired of it.
Genuinely tired.
Every time I’d tail the auth log on my server, it was just wall-to-wall garbage β bots hammering SSH from all over the world, all day, every day.
I’d done the usual stuff: changed the port, set up fail2ban, whitelisted IPs.
And honestly? It still felt like playing whack-a-mole.
So I finally just said forget it and closed the port entirely.
No port, no problem.
The approach is simple: route all SSH access through Cloudflare Tunnel and don’t expose port 22 (or any other port) to the internet at all.
This covers both macOS Terminal and Windows PuTTY, since I use both depending on where I’m working.
Server is Rocky Linux 9.7, for reference.
Install cloudflared on Your Server
cloudflared is the daemon Cloudflare built for their Tunnel product.
On the server it acts as the tunnel endpoint.
On your local machine it acts as the client.
Think of it as a persistent outbound connection that Cloudflare uses to reach your server β no inbound ports required.
On Rocky Linux, grab the RPM directly from GitHub:
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-x86_64.rpm -O cloudflared.rpm
sudo rpm -ivh cloudflared.rpm
cloudflared --version
If a version number shows up, you’re good.
More details at the official repo if you need them:
https://github.com/cloudflare/cloudflared/?tab=readme-ov-file#installing-cloudflared
Set Up the Cloudflare Tunnel
Authenticate
cloudflared tunnel login
This prints a URL in your terminal.
Open it in a browser, log into Cloudflare, and pick the domain you want to use.
When it succeeds, ~/.cloudflared/cert.pem gets saved on the server automatically.
Create the Tunnel
cloudflared tunnel create andy-tunnel
You’ll get a tunnel ID and a path to a credentials file.
Guard that credentials file like a private key β because functionally, it is one.
Write config.yml
vi ~/.cloudflared/config.yml
tunnel: your-tunnel-id-here
credentials-file: /etc/cloudflared/your-tunnel-id-here.json
ingress:
- hostname: ssh.andytips.com
service: ssh://localhost:22
- service: http_status:404
Auto-generate the DNS CNAME
cloudflared tunnel route dns andy-tunnel ssh.andytips.com
One command and the CNAME record shows up in Cloudflare DNS automatically.
You don’t have to touch the dashboard for this part.
You can verify afterward at Cloudflare dashboard β Zero Trust β Networks β Tunnels, and check DNS β Records to confirm ssh.andytips.com is there.
Register as a systemd Service
Copy the config files over and register the service:
sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/config.yml /etc/cloudflared/
sudo cp ~/.cloudflared/your-tunnel-id-here.json /etc/cloudflared/
sudo cp ~/.cloudflared/cert.pem /etc/cloudflared/
Open /etc/cloudflared/config.yml and update the credentials-file path to point to /etc/cloudflared/.
sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
sudo systemctl status cloudflared
active (running) means you’re connected.
Kill the SSH Port in the Firewall
This is the whole point.
Don’t just restrict it β remove it.
sudo firewall-cmd --remove-service=ssh --permanent
sudo firewall-cmd --reload
Port 22 is now completely invisible from the outside.
There’s nothing to scan, nothing to brute force, nothing to probe.
The only way in is through the tunnel.
I’ll be honest, the first time I did this and then successfully SSH’d in through the tunnel anyway, it felt weirdly satisfying.
macOS Client Setup
Install cloudflared
brew install cloudflared
One line, done.
Configure ~/.ssh/config
Host andy
HostName ssh.andytips.com
User your-username-here
ProxyCommand cloudflared access ssh --hostname ssh.andytips.com
ServerAliveInterval 60
ServerAliveCountMax 3
After this, ssh andy from your terminal just works.
No flags, no fumbling around with proxy commands manually.
Windows PuTTY Client Setup
Not gonna lie, Windows takes a bit more effort here.
It’s not hard, just more steps.
Install cloudflared
Open PowerShell as administrator and run:
winget install Cloudflare.cloudflared
After it finishes, open a new PowerShell window β the old one won’t have the updated PATH β and grab the exact path to the binary:
where.exe cloudflared
Save that path.
You’ll need the exact location when configuring PuTTY.
Generate ED25519 Key Pair with PuTTYgen
Windows doesn’t share your existing SSH keys automagically, so you need to generate a new pair.
Fire up PuTTYgen:
- Select EdDSA
- Click Generate and move your mouse around the blank area
- Copy the entire public key text shown at the top
- Save the private key as a
.ppkfile somewhere you’ll remember
Register the Public Key on the Server
From macOS (or anywhere you already have access), add the Windows public key to the server:
echo "paste-your-public-key-here" >> ~/.ssh/authorized_keys
Configure PuTTY
Connection β Data:
- Auto-login username:
your-username
Connection β SSH β Auth β Credentials:
- Private key file: path to your
.ppkfile
Connection β Proxy:
- Proxy type:
Local - Command:
C:\Users\YourName\AppData\Local\Microsoft\WinGet\Links\cloudflared.exe access ssh --hostname ssh.andytips.com
Use whatever path
where.exe cloudflaredreturned β don’t just copy mine.
Also, leave out%host %portfrom the end of that command.
Adding them breaks the conneciton. Ask me how I know.
Session:
- Host Name:
ssh.andytips.com - Port:
22
You type port 22, but the traffic doesn’t actually hit port 22 on the server.
It goes through the tunnel.
The port number here is basically just telling PuTTY what kind of connection to set up.
Security Check Before You Call It Done
Make Sure Password Auth is Off
sudo vi /etc/ssh/sshd_config
PasswordAuthentication no
Confirm this is set to no.
Here’s the thing people sometimes miss β even before you set up any Cloudflare Zero Trust policies, this setup is already pretty solid.
The server only accepts logins where the public key in authorized_keys matches the private key on your local machine.
Someone who somehow gets through the tunnel still can’t log in without the right key.
That’s two layers of defense before you’ve even touched Zero Trust.
Zero Trust Access Policy (Worth Doing Eventually)
Go to Cloudflare Zero Trust β Access β Applications and add a policy for ssh.andytips.com.
You can require email OTP or other MFA right at the tunnel entrance.
For most setups, SSH key auth is plenty.
But if you’re running something sensitive, layer it on.
So Is This Actually Worth It?
Short answer: yes, at least for me.
Here’s what you gain.
Port scanning becomes useless β there’s literally no SSH port to find.
IP whitelisting headaches go away β the tunnel doesn’t care what IP you’re coming from, which is great if you move around a lot or work from different locations.
Cloudflare’s DDoS protection and TLS layer come along for free.
And as a bonus, this also works for servers behind NAT β no port forwarding needed, since it’s all outbound tunnel traffic.
That last one is kind of underrated honestly.
The downsides are real though, and I’m not going to sugarcoat them.
If Cloudflare has an outage, you lose SSH access.
Full stop.
That’s the trade-off for handing infrastucture control to a third party, and it’s worth thinking about before you commit.
You’ll also need to install cloudflared on every machine you want to connect from, which gets mildly annoying after a while.
And yeah β all your traffic flows through Cloudflare.
If that makes you uneasy, that’s a valid concern and not something I can just wave away.
Look, none of this is perfect.
Every security decision is a trade-off.
But for killing off SSH brute force attempts and shrinking your attack surface to basically zero?
This is the best solution I’ve found so far.
Personally, I’ll take this over babysitting fail2ban logs any day.