I used to port-forward 25565 like everyone else, and within a week some bot in another timezone was banging on my Minecraft server with a malformed handshake loop. Switched the whole thing to Tailscale on a Saturday afternoon, and now my router config has zero open ports for games. Writing it down before I forget the ten-minute version.

TL;DR

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh --advertise-tags=tag:gameserver --hostname=mc
sudo ufw deny 25565/tcp
sudo ufw allow in on tailscale0

That's it on the server. Friends install Tailscale, log into the same tailnet, point their game client at mc.tail-scale.ts.net, done. No router, no public IP exposed, no DDoS surface. Below is the actual config and the two things that burned me.

Why not just port-forward?

Because public game ports get scanned. Minecraft especially — there are entire botnets that look for 25565 and try exploits or just keep a TCP socket open to drain RAM. Valheim and Palworld are quieter targets but the same logic applies: if six friends play, you don't need the entire internet to be able to reach the socket.

The two real options are a WireGuard server you run yourself or a mesh VPN like Tailscale. WireGuard is great if you like config files and want zero third-party control plane. Tailscale is great if you want it to just work on your friend's mom's iPhone in fifteen seconds. I run both for different things; this post is the Tailscale path.

If you want Tailscale's protocol but no Tailscale Inc. coordination server, Headscale is a self-hosted control plane that speaks the same client protocol. Post on that coming later — placeholder: /headscale-self-hosted-tailscale/.

Install and auth on the server

Assuming Ubuntu/Debian on the box where the gameserver actually runs (Minecraft, Valheim, Palworld, whatever):

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh --advertise-tags=tag:gameserver --hostname=mc

A URL prints. Open it on your laptop, log in, approve the machine. Done.

A few flags worth knowing:

  • --ssh lets you SSH in over the tailnet using Tailscale's identity-based SSH. You can keep OpenSSH too — but I usually disable password auth and the public 22 entirely, see SSH hardening checklist.
  • --advertise-tags=tag:gameserver is what makes ACLs work later. Tags need to be declared in the ACL file before you can advertise them, otherwise tailscale up errors. Easy to forget on first run.
  • --hostname=mc becomes the MagicDNS name. Pick something short.

After login:

tailscale status
tailscale ip -4

You'll see the server has a 100.x.y.z address — for me it landed on 100.74.12.41. That's the only address friends will ever connect to.

Adding friends to the tailnet

Two ways. Either invite them as users on your tailnet (admin console → Users → Invite), or generate a pre-auth key and have them run tailscale up --authkey=tskey-.... I do the invite path because then each friend has their own identity, which the ACL needs.

Tell them to install Tailscale on whatever they game from — Windows client, macOS, Linux. The mobile clients exist too but nobody plays Minecraft on a phone (mostly).

ACLs so they only reach the gameserver port

This is the part most people skip and then wonder why their friend can SSH into the server. By default a tailnet is flat: every node can reach every node on every port. Lock it down.

Admin console → Access Controls. Replace with something like:

{
  "tagOwners": {
    "tag:gameserver": ["autogroup:admin"]
  },
  "groups": {
    "group:friends": ["alice@example.com", "bob@example.com"]
  },
  "acls": [
    {
      "action": "accept",
      "src":    ["autogroup:admin"],
      "dst":    ["*:*"]
    },
    {
      "action": "accept",
      "src":    ["group:friends"],
      "dst":    ["tag:gameserver:25565,2456-2458,8211"]
    }
  ],
  "ssh": [
    {
      "action": "accept",
      "src":    ["autogroup:admin"],
      "dst":    ["tag:gameserver"],
      "users":  ["root", "ubuntu"]
    }
  ]
}

What this says: I (admin) can do anything. My friends group can only hit the gameserver tag, and only on Minecraft (25565), Valheim (2456–2458 UDP), and Palworld (8211). They cannot SSH, they cannot probe other nodes on my tailnet, they cannot see my home NAS that's also on the tailnet. If a friend's laptop gets compromised, the blast radius is "they can connect to a Minecraft port."

Save. Tailscale validates the JSON and tells you exactly which line is wrong if you typo a comma. Don't ask how I know.

MagicDNS hostname

Admin console → DNS → enable MagicDNS. Now mc (the hostname I gave during tailscale up) resolves to the server's 100.x.y.z for everyone in the tailnet. Friends type mc.tail-scale.ts.net (or whatever your tailnet name is — mc.your-tailnet.ts.net) into the Minecraft "Add Server" dialog and it just works. No IPs to memorize, no DNS records to maintain.

For Valheim and Palworld the clients accept hostnames in the direct-connect field as well. If a launcher insists on an IP, tailscale ip -4 mc will print it.

Firewall: deny public, allow tailscale0

Belt and suspenders. Even though the gameserver might be bound to 0.0.0.0, UFW makes sure nothing public reaches it:

sudo ufw default deny incoming
sudo ufw allow in on tailscale0
sudo ufw deny 25565/tcp
sudo ufw deny 2456:2458/udp
sudo ufw deny 8211/udp
sudo ufw allow 22/tcp comment 'or skip if using tailscale ssh only'
sudo ufw enable

The allow in on tailscale0 line is the magic — anything arriving via the Tailscale interface is fine, anything arriving via eth0/ens3 from the public internet on those game ports is dropped. If you've already moved SSH to Tailscale-only, drop the allow 22 line and you have a server with literally zero inbound public ports except whatever DHCP/ICMP your provider needs.

If you're setting up the Minecraft server itself for the first time, see Minecraft Java server on Linux — that post handles the systemd unit and tuning.

Verify

From your laptop, also on the tailnet:

tailscale ping mc
nc -vz mc.tail-scale.ts.net 25565

tailscale ping will tell you whether the connection is direct (via 100.x.y.z) or relayed through DERP. Direct is faster — for gaming you really want direct. If it stays on DERP, your home router is probably blocking UDP 41641 outbound somehow, or one side is on a hostile CGNAT. Most consumer ISPs are fine.

From a phone off your home network: turn on Tailscale, try connecting. If the friend can join and a stranger on Shodan cannot, you're done.

Gotchas

Auth keys expire — including the server's. By default device keys expire after 180 days. If you don't disable expiry on the gameserver in the admin console, six months from now your friends will scream that the server is gone, and you'll be confused because the box is up. Admin console → Machines → server → Disable key expiry. Do it once, forget about it.

Don't accidentally make the gameserver an exit node. I did this on the second box because I copy-pasted a tailscale up line from another tutorial that had --advertise-exit-node. Suddenly all my friends' phone traffic was being routed through my home connection because one of them clicked "Use as exit node" out of curiosity. Bandwidth bill, confusion, no actual harm but embarrassing. Only advertise exit-node on machines you want to be exit nodes.

ACL JSON is strict and silent about intent. It will validate syntax but not tell you "you forgot to put the server in tagOwners so the tag is meaningless." If tailscale status shows your server with no tag next to it, that's why. The tag has to be declared in tagOwners before the server can advertise it, and the server has to actually re-run tailscale up --advertise-tags=... after the ACL change. I once spent an hour wondering why my rule wasn't matching.

Mobile clients drain battery if friends leave Tailscale on 24/7. Not a huge deal on modern phones but tell friends they can toggle it off when they're not playing. Or better, on iOS/Android use the on-demand option so it only activates when reaching *.ts.net. Saves arguments.

Why this works for me

I run six game servers on one Proxmox host for a group of about ten friends. Before Tailscale, that was six port-forwards, six DDNS entries, and one nervous evening every time someone scanned a port. Now it's one daemon on the host, one ACL file, and the public attack surface is exactly nothing for games. SSH is also tailnet-only via --ssh, so my OpenSSH is firewalled off too.

The only thing I'd warn about: you are trusting Tailscale's coordination server for identity. If that bothers you philosophically — fine, run WireGuard yourself or wait for the Headscale post. For me, the tradeoff is worth it. Ten minutes of setup, friends connect by typing mc into Minecraft, and I haven't thought about the router since.


Related posts