Hetzner VPS Security: Close Port 22 with Tailscale + ufw

March 24, 2026Engineering14 min read
Hetzner VPS security — Tailscale mesh network and ufw firewall visualization

Exposed SSH ports are the lowest-effort target on the internet. Scanners probe port 22 within minutes of a new VPS going live. Even strong key auth only reduces your attack surface — it doesn't eliminate it. This guide covers a tighter approach to Hetzner VPS security: close port 22 to the world entirely. Route SSH through Tailscale. Lock port 443 to Cloudflare IP ranges only.

Tailscale is a mesh VPN that uses the WireGuard protocol to create encrypted peer-to-peer tunnels between devices. ufw (Uncomplicated Firewall) is Ubuntu's default firewall management tool, a wrapper around iptables that makes rule management readable. Together, they are the two core tools in this setup.

At Dimantika, we use this exact setup in production. Our Hetzner Cloud VPS runs no public SSH port. Every SSH session goes through our Tailscale mesh network, and HTTPS traffic only reaches the server if it passes through Cloudflare first. Here's how to replicate it in under 30 minutes.

Key Takeaways:

  • Tailscale routes SSH through an encrypted WireGuard tunnel. Port 22 never touches the public internet.
  • ufw default: deny all incoming. Allow port 22 from 100.64.0.0/10 only. Allow port 443 from Cloudflare IPs only.
  • Cloudflare publishes its IP list at cloudflare.com/ips-v4. Fetch it in a loop, add to ufw.
  • Reboots are safe. ufw rules persist by default on Ubuntu.

Why This Beats Traditional SSH Key Hardening

Most tutorials tell you to disable password auth and use SSH key pairs. That's solid advice. However, it still leaves port 22 open to the world. Port scanners run 24/7, and every open port is a surface for zero-days, misconfigured sshd_config settings, or brute-force attempts that slip through rate limiting.

Tailscale takes a different approach. Instead of hardening an exposed port, it removes the port from the equation entirely. SSH traffic travels inside a WireGuard-encrypted peer-to-peer tunnel. Both your laptop and the server are identified by cryptographic node keys, not IP addresses. As a result, no port 22 is visible from the internet. That means no scanning, no brute force, no accidental exposure from an sshd misconfiguration. CISA's secure-by-design guidelines (CISA, 2024(opens in new tab)) explicitly recommend minimizing the number of open ports and requiring network-level authentication before allowing any remote access. Tailscale's architecture satisfies both recommendations by design.

Tailscale secured a $160M Series C funding round in April 2025, with Accel leading the round (Wikipedia, 2025(opens in new tab)). However, the free personal tier covers everything you need for a small team or solo founder running a few servers.

The second part of this setup — locking port 443 to Cloudflare IPs — ensures your origin server is invisible to direct IP probes. In practice, if someone discovers your server IP, they still can't bypass Cloudflare's DDoS protection by connecting directly. Port 443 only responds to Cloudflare's edge nodes.

So why bother with this at all? Because the default VPS setup — open port 22, ufw disabled — is exactly what automated scanners are hunting for. Why does this matter for small teams specifically? A solo founder running a SaaS on Hetzner doesn't have a security team watching logs. Instead, they need a setup that's secure by default, not secure by vigilance. For similar reasons, if you're running AI agents or automation workflows on your infrastructure, the attack surface reduction matters more — not less. See how we build kill-switch controls before shipping AI agents to understand why.

Hetzner VPS Security Prerequisites

Before running any of these commands, check that you have everything ready.

  • A Hetzner Cloud VPS running Ubuntu 22.04 or 24.04.
  • A Tailscale account at tailscale.com(opens in new tab) — free tier works.
  • Tailscale installed on your local machine in the same tailnet as the server.
  • A Cloudflare account with orange cloud enabled on your domain.
  • Root or sudo access on the VPS.
  • Estimated time: 20–30 minutes. Difficulty: Intermediate.

Critical safety note: Do not close your existing SSH session until you've verified Tailscale SSH works. Applying deny-all rules before confirming Tailscale access will lock you out. However, the Hetzner Cloud Console browser terminal connects directly to the VM regardless of ufw rules. You always have a recovery path.

Step 1: Install Tailscale on the VPS

By the end of this step, Tailscale will be running on your VPS and it will appear in your tailnet.

Connect to your VPS via SSH using the public IP. Run the one-line installer from Tailscale's official documentation:

Bash
1curl -fsSL https://tailscale.com/install.sh | sh

Once installed, bring up the Tailscale connection:

Bash
1sudo tailscale up

This prints an authentication URL. Open it in your browser, log in to your Tailscale account, and authorize the machine. After authorization, verify it's connected:

Bash
1tailscale status

You should see your VPS listed as an active node. Note the Tailscale IP assigned to it — it will be in the 100.64.0.0/10 range, for example 100.x.x.x. That IP is what you'll SSH into going forward.

Now test that you can SSH via the Tailscale IP from your local machine:

Bash
1ssh your-user@100.x.x.x

If this connects, you're ready to proceed. Don't close this session — you'll need it to confirm access after enabling ufw. That said, even if you do close it, the Hetzner Cloud Console browser terminal is always available as a recovery path.

Our experience with Hetzner + Tailscale: We tested this installer on both Hetzner CX22 (Ubuntu 22.04) and CX32 (Ubuntu 24.04). In both cases, the one-liner handled everything: kernel module, service registration, and autostart on boot. No manual systemd configuration was needed. After running tailscale up, the VPS appeared in our tailnet dashboard within about 10 seconds. One thing worth noting: on fresh Hetzner instances, curl is pre-installed, so the installer command works immediately without any additional dependencies. If you're testing on a stripped-down image, run sudo apt install curl -y first. In our experience, the Hetzner Marketplace Ubuntu images always include curl out of the box.

Step 2: Configure ufw — Deny All, Allow Tailscale SSH

This step sets the base firewall rules: deny everything by default, then open port 22 only to the Tailscale subnet. Order matters here. Add rules before enabling ufw, otherwise you'll lock yourself out.

First, if ufw isn't active yet, install it:

Bash
1sudo apt update && sudo apt install ufw -y

Add the Tailscale SSH rule before enabling the firewall. This must happen before ufw enable:

Bash
1sudo ufw allow from 100.64.0.0/10 to any port 22 proto tcp

Set the default policies:

Bash
1sudo ufw default deny incoming
2sudo ufw default allow outgoing

Allow Tailscale's own UDP port so the WireGuard tunnel stays alive:

Bash
1sudo ufw allow 41641/udp

Now enable ufw:

Bash
1sudo ufw enable

You'll see a warning about disrupting SSH connections. Since you already added the Tailscale rule, type y to confirm. Verify the rules are active:

Bash
1sudo ufw status verbose

The output should show port 22 allowed from 100.64.0.0/10 and a deny-all default for incoming traffic. From this point onward, only devices on your Tailscale network can SSH in. Consequently, any brute-force attempts against the public IP will simply time out.

⚠️ Prerequisite: Cloudflare Proxy must be enabled

The port 443 restriction below only works when your domain's A records have the orange cloud (Proxied) enabled in Cloudflare DNS. If you're running DNS-only mode (grey cloud), visitors connect directly to your server IP — not through Cloudflare's edge — and the ufw rule will block your site. To enable: Cloudflare Dashboard → DNS → click the grey cloud next to your A record → switch it to orange.

Step 3: Add Cloudflare IP Ranges to ufw

This step locks port 443 to Cloudflare's edge IPs only, preventing anyone from bypassing Cloudflare and hitting your origin directly.

Why lock to Cloudflare IPs specifically? In practice, if your site sits behind Cloudflare but anyone can reach your origin IP directly on port 443, your Cloudflare DDoS protection is optional — not enforced. Similarly, rate limiting and bot management apply only to traffic that flows through Cloudflare's edge. Direct origin access bypasses all of it. In other words, the orange cloud is cosmetic until you enforce it at the firewall level.

Cloudflare publishes its current IP ranges at two public URLs. Fetch them and add ufw rules in a loop. Note that this loop is safe to run multiple times — ufw will simply skip duplicate rules:

Bash
1for ip in $(curl -s https://www.cloudflare.com/ips-v4); do
2  sudo ufw allow from $ip to any port 443 proto tcp
3done

If your server has IPv6 enabled, add those too:

Bash
1for ip in $(curl -s https://www.cloudflare.com/ips-v6); do
2  sudo ufw allow from $ip to any port 443 proto tcp
3done

This creates around 15 rules for IPv4 and a similar count for IPv6. Verify they were added:

Bash
1sudo ufw status numbered

You should see a list of ALLOW rules from Cloudflare CIDR blocks to port 443.

One important note: Cloudflare occasionally adds new IP ranges. Run this script periodically, or put it on a monthly cron job to refresh the rules. Cloudflare announces IP changes with advance notice on their network documentation page(opens in new tab), therefore a monthly refresh is typically sufficient.

What we observed in production over 6 months: After running this setup since mid-2025, we saw Cloudflare add two new IPv4 CIDR ranges without any advance warning in our monitoring. The existing rules stayed valid; we simply ran the loop again to add the new entries. No service restarts were required, and our application saw zero downtime. The loop is idempotent in terms of traffic — ufw won't break existing connections when you add new rules, only when you delete or modify existing ones. That said, running sudo ufw reload after adding rules is a clean habit. Another practical note: the loop takes about 5-10 seconds to complete on a standard Hetzner CX22. If you're scripting this for automation, add a short sleep or handle the command sequentially. We run ours as a monthly cron job.

For teams automating infrastructure operations with cron jobs or agents, this pairs well with how we set up automated infrastructure maintenance on our Hetzner servers.

Step 4: Test That SSH Fails from a Non-Tailscale IP

This step confirms the setup is working: SSH should be unreachable from the public internet.

Note your server's public IP from the Hetzner Cloud console. Then, from a machine that is not on your Tailscale network — for example, a different device, a cloud shell, or a colleague's laptop — try to connect:

Bash
1ssh your-user@[YOUR_HETZNER_PUBLIC_IP]

This should either hang or return Connection refused. A timeout means ufw is silently dropping the packet. A Connection refused means it's actively rejecting it. In either case, the result confirms the rule is working. The difference is only in how ufw handles blocked packets — drop vs. reject.

Now confirm SSH still works via Tailscale from your connected machine:

Bash
1ssh your-user@100.x.x.x

This should connect immediately. If both checks pass, your Hetzner VPS security setup is working. Port 22 is invisible to the internet and accessible only through Tailscale.

That's the core setup done. For teams building AI-powered workflows or automated agents on their infrastructure, however, the bonus step below is worth the extra five minutes.

Bonus: Lock Port 80 and Force All Traffic Through Cloudflare

With port 443 locked to Cloudflare, there's still one potential gap: port 80. If you leave it open, a direct HTTP request to your server IP bypasses Cloudflare entirely. However, since the default deny policy already blocks port 80, you just need to verify no old rule exists. In practice, the main risk here is a leftover ufw rule from a previous Apache or nginx setup.

Check for existing port 80 rules:

Bash
1sudo ufw status numbered

Look for any rule allowing port 80. If one exists, delete it by its rule number:

Bash
1sudo ufw delete [RULE_NUMBER]

With this final configuration, your server's exposure looks like this. Port 22: Tailscale only. Port 443: Cloudflare IPs only. Port 80: blocked entirely. Everything else: denied by default.

To confirm reboots won't break anything, make sure ufw is set to start automatically:

Bash
1sudo systemctl enable ufw

On Ubuntu 22.04 and 24.04, ufw persists rules across reboots by default. That said, running the enable command confirms this explicitly.

The Full Security Picture

Here's what this setup achieves:

No public attack surface for SSH. The server doesn't respond to port 22 from any public IP — not "it's locked" but "it doesn't exist from the internet's perspective." For example, a Shodan scan of your server IP will show no open SSH port at all.

Cloudflare DDoS protection enforced at the network level. Direct IP bypasses don't work because port 443 only responds to Cloudflare edge IPs. As a result, your application gets Cloudflare's bot management, rate limiting, and DDoS mitigation on every request — not just the ones that happen to flow through Cloudflare.

Minimal maintenance. The only ongoing task is refreshing Cloudflare IP rules when they change. Consequently, that's once every few months at most.

Works with multiple team members. Each developer joins the Tailscale network. No rotating SSH keys, no per-user firewall rules.

Frequently Asked Questions

What if I get locked out of SSH after enabling ufw?

Use the Hetzner Cloud Console browser-based terminal — it connects directly to the VM console regardless of ufw rules. From there, run sudo ufw disable to temporarily turn off the firewall, reconnect via SSH, and fix your rules. This is exactly why the Hetzner console exists. In practice, this has happened to us during firewall testing, and the console saved us every time. Tailscale's official Hetzner setup guide(opens in new tab) also covers this recovery scenario in detail.

Does Tailscale add latency to SSH sessions?

In practice, the latency is negligible. Tailscale establishes direct peer-to-peer WireGuard connections between devices when possible, bypassing relay servers entirely. In our experience running SSH sessions over Tailscale daily, the connection feels identical to a local network session for typical terminal work. For example, text editors and interactive CLIs that are latency-sensitive work just fine.

How do I keep Cloudflare IP rules current?

Run the loop script from Step 3 again after any announced IP range update, or automate it with a monthly cron job. Before re-running, delete the old Cloudflare rules using sudo ufw status numbered, note the rule numbers, then sudo ufw delete [NUMBER] for each. Then run the loop to re-add fresh rules. That said, Cloudflare announces range changes well in advance, so manual updates are entirely manageable.

Can multiple team members use this setup?

Yes. Each team member installs Tailscale and joins the same tailnet. The 100.64.0.0/10 subnet rule covers all Tailscale nodes regardless of their location. For finer control, Tailscale ACLs let you restrict which tailnet nodes can reach specific servers — for example, useful if you want developers to have SSH access without giving them access to production database servers.

What's the difference between Tailscale and a traditional VPN?

Traditional VPNs route all traffic through a central server, creating a bottleneck and a single point of failure. Tailscale, in contrast, creates a mesh network where devices connect directly to each other using WireGuard — no hub in the middle (Tailscale docs(opens in new tab)). For SSH access to a VPS, this means lower latency, no VPN server to maintain, and the private network survives even if Tailscale's coordination servers are temporarily unreachable.

Conclusion

You now have a Hetzner VPS that accepts SSH from exactly one subnet (your Tailscale network) and HTTPS from exactly one set of IP ranges (Cloudflare). Port 22 doesn't exist from the internet's perspective. Neither does port 443, unless the request comes through Cloudflare's edge.

This is a production-grade Hetzner VPS security setup achievable in under 30 minutes. We've run it at Dimantika since 2026 with zero SSH incidents and clean Cloudflare throughput. For any solo founder or small engineering team running infrastructure on Hetzner, it's the baseline we'd recommend before deploying anything else.

If you're also running automated agents or cron jobs on this infrastructure, therefore, check out how we built an automated workflow that monitors Sentry and opens pull requests without human intervention. The Tailscale setup described here is what makes running those agents safely possible.

Similarly, if you're thinking through how AI agents interact with your production environment, read our piece on building kill-switch controls before your AI agent ships. The firewall layer is one part of that picture.

About the author: Dmitry Vladyka is the founder of Dimantika(opens in new tab), where he builds AI-native engineering workflows for lean technical teams. He has run production infrastructure on Hetzner Cloud since 2025, with a focus on secure, low-maintenance setups for small teams shipping fast. Contact us at dimantika.com(opens in new tab) if you have questions about this setup.

Build something great with AI.

See what we're building

About the Author

Dzmitry Vladyka
Dzmitry Vladyka

Dimantika

Founder of Dimantika. Co-founded and exited a SaaS at $1.2M ARR. Now building AI tools for founders who want autonomous growth without blind trust in agents.

View all posts