Getting a public IP with Tailscale and Google Cloud
June 26, 2026
Oops, I didn’t do my due diligence, and the new place I’ve moved into has a terrible Internet connection with no port forwarding allowed. Guess it’s time to dig through memory lane to figure out my options!
Previously, I wrote about getting a public IP with reverse SSH tunneling, but that means all the traffic gets wrapped up under the SSH protocol. I’m hosting websites and services, and thus TCP-on-TCP is not going to be a very good experience.
Well, there’s this really neat blog post from Ersei that uses Wireguard, but I don’t want to use Wireguard in addition to Tailscale. I guess I’ll have to rip them off and document what I did for getting a publicly routable IP with Tailscale. (Sorry Ersei)
Step 1: Get a public VPS
Google Cloud has a free e2-micro instance in the free tier? Yes please!
Keep in mind the region is restricted, and as of writing this post, it’s restricted to Oregon, Iowa, and South Carolina. See here for details.
Since I don’t want to dig around in the console any more than I have to, here’s
a copy of the equivalent gcloud CLI tool command:
gcloud compute instances create public-ip-vm \
--project=public-ip-vm \
--zone=us-east1-c \
--machine-type=e2-micro \
--network-interface=network-tier=PREMIUM,stack-type=IPV4_ONLY,subnet=default \
--metadata=enable-osconfig=TRUE \
--can-ip-forward \
--maintenance-policy=MIGRATE \
--provisioning-model=STANDARD \
--host-error-timeout-seconds=330 \
--service-account=redacted-compute@developer.gserviceaccount.com \
--scopes=https://www.googleapis.com/auth/devstorage.read_only,https://www.googleapis.com/auth/logging.write,https://www.googleapis.com/auth/monitoring.write,https://www.googleapis.com/auth/service.management.readonly,https://www.googleapis.com/auth/servicecontrol,https://www.googleapis.com/auth/trace.append \
--enable-display-device \
--tags=http-server,https-server \
--create-disk=auto-delete=yes,boot=yes,device-name=public-ip-vm,image=projects/debian-cloud/global/images/debian-13-trixie-v20260609,mode=rw,size=30,type=pd-standard \
--shielded-secure-boot \
--shielded-vtpm \
--shielded-integrity-monitoring \
--labels=goog-ops-agent-policy=v2-template-1-7-0,goog-ec-src=vm_add-gcloud \
--reservation-affinity=any \
&& \
printf 'agentsRule:\n packageState: installed\n version: latest\ninstanceFilter:\n inclusionLabels:\n - labels:\n goog-ops-agent-policy: v2-template-1-7-0\n' > config.yaml \
&& \
gcloud compute instances ops-agents policies create goog-ops-agent-v2-template-1-7-0-us-east1-c \
--project=public-ip-vm \
--zone=us-east1-c \
--file=config.yaml
Yeah… on second thought you may want to just use the web console.
Step 2: Set up DNS if necessary
You may want to set up a DNS entry to your new VPS’s public IP address.
Step 3: Install nftables and Tailscale
As you probably noticed above, I used Debian 13:
sudo apt update
sudo apt upgrade -y
sudo apt install -y nftables
curl -fsSL https://tailscale.com/install.sh | sh
Step 4: Configure Tailscale
Run sudo tailscale up and follow the prompts.
Before approving it on your network, you may want to assign it a tag and set up ACL rules so that only the target local machine can communicate with it, and so that it can’t access any other machine on your Tailnet.
Step 5: Configure nftables
This is where I steal most of Ersei’s configuration.
Create /etc/nftables/proxy.nft:
table ip nat {
chain PREROUTING {
type nat hook prerouting priority dstnat; policy accept;
meta l4proto tcp tcp dport 80 dnat to 100.67.67.67:80;
meta l4proto tcp tcp dport 443 dnat to 100.67.67.67:443;
}
chain INPUT {
type nat hook input priority 100; policy accept;
}
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
# Masquerade Tailscale traffic
oifname "tailscale0" masquerade;
}
chain OUTPUT {
type nat hook output priority -100; policy accept;
}
}
Replace 100.67.67.67 with the Tailscale IP address of the local machine you
want to expose to the Internet.
Notice that with Tailscale we want to masquerade the traffic; otherwise the return path will not work and your connection will time out!
Also keep in mind that the original configuration drops all ICMP traffic, but I
like to test if hosts are up using ping so I’d rather not do that. Just add back
the drop line back from Ersei’s config if you don’t need ICMP traffic.
Next is /etc/nftables/main.nft:
# drop any existing nftables ruleset
flush ruleset
# a common table for both IPv4 and IPv6
table inet nftables_svc {
# protocols to allow
set allowed_protocols {
type inet_proto
elements = { icmp, icmpv6 }
}
# interfaces to accept traffic on
set allowed_interfaces {
type ifname
elements = { "lo", "tailscale0" }
}
# TCP services to allow
set allowed_tcp_dports {
type inet_service
elements = { ssh, http, https }
}
# UDP services to allow
# I didn't have any but uncomment if you do
#set allowed_udp_dports {
# type inet_service
# elements = { 25565 }
#}
# this chain gathers all accept conditions
chain allow {
ct state established,related accept
meta l4proto @allowed_protocols accept
iifname @allowed_interfaces accept
tcp dport @allowed_tcp_dports accept
#udp dport @allowed_udp_dports accept
}
# base-chain for traffic to this host
chain INPUT {
type filter hook input priority filter + 20
policy accept
jump allow
}
}
include "/etc/nftables/proxy.nft"
We then need to include this chain in /etc/nftables.conf:
# ... other contents of /etc/nftables.conf
include "/etc/nftables/main.nft";
Now enable nftables:
sudo systemctl enable --now nftables
sudo systemctl restart nftables
Step 6: Enable IP forwarding
sudo sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
And you’re done! You should now be able to connect to your local services via the new VPS IP or DNS entry you set up earlier.