TL;DR: Block every incoming port. Use Tailscale as your private mesh network. Move SSH to a non-standard port above 1000, disable root and password auth, and lock it down with SSH keys. Add fail2ban for automatic IP bans. Deploy honeypots on common ports to trap and log attackers. Use Cloudflare Tunnel for any public web traffic. Your server stays reachable only from your devices.
The old way is suicide
Most solo developers set up a VPS like this:
Buy server
Enable root login with password
SSH in on port 22
Start developing
Maybe install UFW if you’re feeling fancy
That is not a server. That is a target.
When OVH deleted my VPS after it got compromised, I learned the question isn’t “how do I secure my server?” The question is: “how do I remove my server from the public internet entirely?”
I wrote the full story of how OVH deleted my VPS in 4 days here.
Here is the stack I rebuilt on, piece by piece, with the exact logic behind each layer.
Layer 1: The mesh (Tailscale)
A friend pointed me to Tailscale. I assumed it was a paid enterprise tool. It’s not. The solo-dev plan is free and powerful enough to run your entire infrastructure.
What it does: Tailscale creates a private mesh network (a WireGuard-based overlay) between your devices. Your laptop, your phone, your VPS, and your home server all talk to each other as if they’re on the same LAN. To the rest of the internet, your server is a black hole.
The philosophy: Your VPS doesn’t need to be “reachable” from the internet. It only needs to be reachable from your devices.
I installed Tailscale on my new server and on my local machine:
# On the server (Debian/Ubuntu)
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# Copy the Tailscale IP shown (e.g., 100.x.x.x)
tailscale ip -4
Then I did something that felt illegal: I blocked all incoming ports. Even 22. Even 443. The server has no public-facing doors.
# Block everything incoming with UFW
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 100.64.0.0/10 # Allow Tailscale mesh network only
sudo ufw enable
I access everything through Tailscale’s private IP. SSH, web servers, and APIs all route over the mesh.
The one exception: I left one non-standard SSH port open as a backup. Not port 22. Something higher. If Tailscale’s coordination server ever goes down, I have a manual fire escape. But that port is locked down by everything that follows.
Layer 2: The moving target (non-standard SSH)
Port 22 is where every automated bot on earth knocks first.
The fix: Move SSH to a port above 1000. Pick something random. Don’t tell anyone. Update your SSH config:
# /etc/ssh/sshd_config
Port 2222 # or whatever you chose
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Run the command to restart
sudo systemctl restart sshd
Now the script-kiddie bots scanning port 22 hit a closed door.
Layer 3: The bouncer (fail2ban)
Even on a hidden port, someone will probe. fail2ban watches your logs and bans IPs that fail repeatedly.
What it does: If an IP tries to brute-force SSH (or any other service) and fails 3 times, fail2ban adds them to an IPTables ban list. Silent. Automatic.
sudo apt install fail2ban
sudo systemctl enable fail2ban
Create /etc/fail2ban/jail.local :
[sshd]
enabled = true
port = 2222 # Your non-standard SSH port
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400 # 24 hours
findtime = 600 # 10 minute window
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Failed login attempts don’t just fail; they get the IP jailed for 24 hours.
Layer 4: No passwords, no root
This was the hardest habit to break. I was used to root + password.
Step 1: Create a normal user with sudo
Never log in as root. Create a dedicated user and give it sudo privileges:
adduser sarvish
usermod -aG sudo myname
Step 2: Copy your SSH public key to the server
Run this from your local machine to push your key:
ssh-copy-id -p [YOUR_PORT] sarvish@[TAILSCALE_IP]
Step 3: Lock down SSH
Edit /etc/ssh/sshd_config on the server to disable root and password auth:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
sudo systemctl restart sshd
If your key isn’t on the server, you are not getting in. Period.
Layer 5: The honey trap (honeypot)
A server friend told me: “Install a honeypot on frequently attacked ports.” I had never considered this.
What it is: A honeypot like cowrie or ssh-honeypot opens fake ports that look like real services. When a hacker scans you and tries to exploit these decoy ports, the honeypot logs everything, wastes their time, and depending on your setup can trigger an immediate IP ban.
Why it works: Attackers don’t know which ports are real and which are traps. They touch a honeypot port, and they’ve already revealed themselves as malicious before they even get near your actual services.
I installed a lightweight honeypot called ssh-honeypot on a few common ports:
Step 1: Install build dependencies
sudo apt install build-essential libssh-dev
Step 2: Clone and build
git clone https://github.com/droberson/ssh-honeypot.git
cd ssh-honeypot
make
Step 3: Generate an RSA key for the honeypot
The honeypot needs its own SSH host key to look like a real server:
ssh-keygen -t rsa -b 2048 -f ssh-honeypot.rsa -N ""
Step 4: Run the honeypot on a decoy port
sudo ./ssh-honeypot -p 2223 -r ssh-honeypot.rsa
Why port 2223? It looks like an alternative SSH port. Bots scanning for SSH will hit it, try to log in, and get trapped while the honeypot logs every command they attempt.
Now my server isn’t just hidden; it’s actively baiting and trapping anyone who comes knocking.
Layer 6: Cloudflare Tunnel (for web traffic)
If I need to expose a web app or API, I don’t open port 443 on the server.
I use Cloudflare Tunnel (cloudflared). It creates an outbound-only connection from my server to Cloudflare’s edge. The public internet talks to Cloudflare. Cloudflare talks to my server through the tunnel. My server never accepts a direct inbound connection.
# Install cloudflared
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
# Authenticate (opens browser)
cloudflared tunnel login
# Create and run tunnel
cloudflared tunnel create myapp
cloudflared tunnel route dns myapp myapp.mydomain.com
cloudflared tunnel run myapp
You get zero open ports, zero DDoS exposure, and zero certificate management headaches.
What I’m using now
I moved to a European server. Day 2, and it’s running smooth. The difference isn’t the provider. The difference is the architecture.
Everything sits behind Tailscale. SSH is on a ghost port with key-only auth. fail2ban watches the logs. The honeypot is catching probes. Cloudflare handles public traffic. I don’t fear port scans anymore because, to the public internet, my server doesn’t exist.
It took one catastrophic deletion to learn this. You don’t need to lose your server to get it right.

Top comments (0)