# Remote Dev Server Guide Part 3: Security with Tailscale VPN Date: 2026-03-07 · 6 min read Tags: hetzner, tailscale, security, ssh, vpn URL: https://ahmedelywa.com/blog/hetzner-remote-dev-part-3-security-with-tailscale --- In [Part 2](/blog/hetzner-remote-dev-part-2-initial-server-setup), 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](https://tailscale.com) 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 ```bash 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: ```bash 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](https://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: ```bash 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: ```bash 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 curve25519-sha256@libssh.org,curve25519-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,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: ```bash 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: ```bash # 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: ```bash sudo systemctl restart sshd ``` **Important**: Before closing your current SSH session, open a new terminal and verify you can connect via Tailscale: ```bash 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: ```bash 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: ```bash 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: ```bash ssh hetzner ``` ## The Security Model Let's recap what we've achieved: ```mermaid 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](https://blink.sh) 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](https://tailscale.com/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](/blog/hetzner-remote-dev-part-4-claude-code-dev-workflow), 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](/blog/hetzner-remote-dev-part-1-choosing-your-server) 2. [Initial Server Setup](/blog/hetzner-remote-dev-part-2-initial-server-setup) 3. **Security with Tailscale VPN** (you are here) 4. [Claude Code & Development Workflow](/blog/hetzner-remote-dev-part-4-claude-code-dev-workflow) 5. [Production Deployment with GitHub Actions & PM2](/blog/hetzner-remote-dev-part-5-production-deployment)