VPS Hardening Guide (Ubuntu + Tailscale SSH)
Hardening steps for a fresh Ubuntu VPS. Replaces public SSH with Tailscale SSH so the server has zero public attack surface beyond ports 80/443.
Prerequisites: Fresh Ubuntu VPS with root SSH access and a Tailscale account.
1. Update system
apt update && apt upgrade -y
2. Install security tools
apt install -y ufw fail2ban unattended-upgrades
Since we’re disabling public SSH, fail2ban won’t have much to guard by default. If you’re running a web server (nginx, Caddy, etc.), configure fail2ban to watch its logs:
sudo tee /etc/fail2ban/jail.local << 'EOF'
[nginx-http-auth]
enabled = true
[nginx-botsearch]
enabled = true
EOF
sudo systemctl restart fail2ban
3. Create a non-root user
adduser deploy
usermod -aG sudo deploy
Verify sudo works:
su - deploy
sudo whoami # should print "root"
exit
4. Disable root account
passwd -l root
Also prevent root SSH login (belt and suspenders):
sed -i 's/^PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart ssh
5. Enable automatic security updates
dpkg-reconfigure -plow unattended-upgrades
# Select "Yes"
6. Configure firewall
ufw default deny incoming
ufw default allow outgoing
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 22/tcp # temporary — removed after Tailscale is confirmed
ufw enable
7. Install Tailscale & enable Tailscale SSH
You can verify the install script before running it, or install from Tailscale’s official apt repo instead:
curl -fsSL https://tailscale.com/install.sh -o install-tailscale.sh
less install-tailscale.sh # review it
sh install-tailscale.sh
tailscale up --ssh
A login URL will appear — open it in your browser and authenticate.
Once connected, lock down who can SSH into this machine via your Tailscale ACLs. In the Tailscale admin console, restrict SSH access to only your user or specific groups rather than leaving it open to the entire tailnet.
8. Verify Tailscale SSH works
From your local machine (must also have Tailscale running):
ssh deploy@<tailscale-hostname>
Do not proceed until this works. You will lock yourself out otherwise.
9. Disable public SSH
sudo ufw delete allow 22/tcp
sudo systemctl disable --now ssh
sudo systemctl disable --now ssh.socket
10. Harden sysctl
sudo tee /etc/sysctl.d/99-hardening.conf << 'EOF'
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# IPv6 hardening (or disable entirely if you don't use it)
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
EOF
sudo sysctl --system
11. Basic log monitoring
Keep an eye on what’s happening on your server:
# Failed login attempts
sudo journalctl -u ssh --no-pager | grep "Failed"
# fail2ban status
sudo fail2ban-client status
# Recent firewall blocks
sudo dmesg | grep -i "UFW BLOCK"
For something more hands-off, consider setting up logwatch for daily email summaries:
sudo apt install -y logwatch
sudo logwatch --output mail --mailto [email protected] --detail high
Result
- No public SSH — only accessible via Tailscale
- Firewall allows only HTTP (80) and HTTPS (443)
- Automatic security updates enabled
- Root account disabled
- fail2ban running
- Network stack hardened
Connecting
ssh deploy@<tailscale-hostname>