Skip to content
Back to blog

Remote Dev Server Guide Part 3: Security with Tailscale VPN

6 min read
View as Markdown

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

  1. Choosing Your Server
  2. Initial Server Setup
  3. Security with Tailscale VPN (you are here)
  4. Claude Code & Development Workflow
  5. Production Deployment with GitHub Actions & PM2