Remote Dev Server Guide Part 3: Security with Tailscale VPN
In Part 2, we set up our development environment. Right now, our server has SSH open on port 22, accepting connections from the entire internet. Even with key-based auth, this is a target for brute-force attacks, port scanners, and exploits against OpenSSH itself.
The fix: make SSH only accessible through Tailscale VPN. If you're not on the Tailscale network, the server doesn't even respond.
What is Tailscale?
Tailscale creates a private mesh VPN using WireGuard. Every device you install it on gets a stable IP in the 100.x.x.x range. These devices can talk to each other directly, even behind NATs and firewalls, without exposing any ports to the public internet.
Key benefits:
- Zero configuration networking — no port forwarding, no firewall rules to manage
- Identity-based access — tied to your Google/GitHub/Microsoft account
- Works everywhere — macOS, Linux, Windows, iOS, Android
- Free for personal use — up to 100 devices
Install Tailscale on Your Server
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
This prints a URL. Open it in your browser to authenticate with your Tailscale account. Once authenticated, your server gets a Tailscale IP:
tailscale ip -4
# 100.x.y.z (your Tailscale IP — save this, you'll need it everywhere)
Install Tailscale on Your Devices
Install the Tailscale app on every device you want to access the server from:
- Mac: Download from tailscale.com/download or
brew install --cask tailscale - iPhone/iPad: Install from the App Store
- Windows: Download from the website
- Linux: Same curl command as the server
Once all devices are connected, you can see them:
tailscale status
# 100.x.y.z hetzner-dev you@ linux -
# 100.x.y.w macbook-pro you@ macOS active
# 100.x.y.v iphone you@ iOS active
You can now SSH to your server's Tailscale IP from any of your devices, even from a coffee shop or airport WiFi.
Harden SSH Configuration
Now let's lock down SSH so it only listens on the Tailscale interface and uses a non-standard port. Create a hardened SSH config:
sudo tee /etc/ssh/sshd_config.d/hardened.conf << 'EOF'
# Disable password authentication
PasswordAuthentication no
# Disable root login with password (only key)
PermitRootLogin no
# Disable empty passwords
PermitEmptyPasswords no
# Limit authentication attempts
MaxAuthTries 3
# Disable X11 forwarding
X11Forwarding no
# Disable TCP forwarding (unless needed)
AllowTcpForwarding yes
# Set idle timeout (5 minutes)
ClientAliveInterval 300
ClientAliveCountMax 2
# Use strong algorithms
KexAlgorithms [email protected],curve25519-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256
Ciphers [email protected],[email protected],[email protected],aes256-ctr,aes192-ctr,aes128-ctr
MACs [email protected],[email protected],hmac-sha2-512,hmac-sha2-256
EOF
Change the SSH Port
Edit the main config to listen only on Tailscale IP, on a custom port:
sudo sed -i 's/^Port 22$/Port 1993/' /etc/ssh/sshd_config
Why port 1993? It's just an arbitrary non-standard port. Automated scanners target port 22 — using a different port eliminates most noise from your auth logs.
Bind SSH to Tailscale Only
Add a ListenAddress directive so SSH only accepts connections on the Tailscale interface:
# Replace 100.x.y.z with YOUR Tailscale IP from `tailscale ip -4`
echo "ListenAddress 100.x.y.z" | sudo tee -a /etc/ssh/sshd_config
echo "ListenAddress 127.0.0.1" | sudo tee -a /etc/ssh/sshd_config
This is the key security measure. SSH is now invisible to the public internet. Even if someone scans every port on your server's public IP, they won't find SSH.
Restart SSH:
sudo systemctl restart sshd
Important: Before closing your current SSH session, open a new terminal and verify you can connect via Tailscale:
ssh -p 1993 dev@YOUR_TAILSCALE_IP
If it works, you're good. If not, use the Hetzner rescue console to fix the config.
Set Up UFW Firewall
Even with SSH bound to Tailscale, a firewall adds defense-in-depth. Allow only what's needed:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 80/tcp # Nginx HTTP
sudo ufw allow 443/tcp # Nginx HTTPS
sudo ufw allow in on tailscale0 # Allow all traffic on Tailscale interface
sudo ufw enable
The allow in on tailscale0 rule lets SSH (and any other traffic) flow freely over the Tailscale interface, while blocking everything else from the public internet. Verify:
sudo ufw status
Update Your SSH Config (Client Side)
On your Mac (or any device you SSH from), add this to ~/.ssh/config:
Host hetzner
HostName YOUR_TAILSCALE_IP
User dev
Port 1993
IdentityFile ~/.ssh/id_ed25519
Now you can just type:
ssh hetzner
The Security Model
Let's recap what we've achieved:
flowchart TB
subgraph Internet["Public Internet"]
A["Port scanning → nothing found\nSSH brute force → can't connect"]
end
subgraph Server["Hetzner Server"]
B["Nginx (80/443) → public web"]
C["SSH (1993) → Tailscale only"]
D["Apps (3001-3005) → localhost\nDBs (5432-5434) → localhost\nRedis (6379-6380) → localhost"]
end
subgraph Devices["Your Devices (Tailscale)"]
E["MacBook → ssh hetzner"]
F["iPhone → Termius app"]
G["iPad → Termius app"]
end
Internet -->|"ports 80/443 only"| B
Devices -->|"WireGuard tunnel"| C
Only your Tailscale-authenticated devices can reach SSH. The public internet can only hit Nginx on ports 80 and 443 for serving your websites. All application ports and databases are bound to 127.0.0.1.
Working from Your Phone
This setup means you can SSH into your server from your iPhone. Apps like Blink Shell or Termius support SSH with Tailscale. As long as Tailscale is running on your phone, you can connect from anywhere — home, office, train, airport.
I regularly check on things from my iPhone using Tailscale. The connection is fast because WireGuard is lightweight and Tailscale handles NAT traversal automatically.
Optional: Tailscale SSH
Tailscale also offers Tailscale SSH, which replaces OpenSSH entirely. It authenticates using your Tailscale identity instead of SSH keys. I prefer keeping OpenSSH for compatibility, but Tailscale SSH is worth exploring if you want even simpler access management.
What's Next
The server is now secure. In Part 4, we'll install Claude Code and set up a development workflow that lets you build features with AI assistance from anywhere.
Series Navigation
- Choosing Your Server
- Initial Server Setup
- Security with Tailscale VPN (you are here)
- Claude Code & Development Workflow
- Production Deployment with GitHub Actions & PM2