How to set up a private web site with Tailscale in 5 minutes
I have a personal collection of HTML documents that I want available to me when I’m on the go, but that I don’t want or need to host on public internet.
To do that, I need a web server that I can access, but you can’t.
Tailscale makes it really easy to set up a network of devices connected via Wireguard tunnels. In other words, it gives you a VPN that doesn’t suck. Not only does it not suck, it works really well.
Here’s the basic recipe for setting up a private website:
- Connect via Tailscale
- Firewall to only permit Tailscale IP
- Web server listens only on the Tailscale IP
- Use Tailscale certs for TLS
The whole process takes less than 10 minutes - 5 if you’re quick with it.
I’ll be running this on a FreeBSD host in GCP. The commands may be different on your machine, but the general principles and techniques are the same.
Add a host to your Tailnet
Note: I like to upgrade any existing packages before installing new packages.
Once you have a Tailscale account, install Tailscale on your host and add it to your Tailnet:
# pkg install -y tailscale
# service tailscaled enable
# service tailscaled start
# tailscale up
This will print an auth URL to the terminal. Open it in your browser, go through the auth process, and your host will be on the Tailnet.
Serve a directory with nginx
Install nginx, and point it at a directory with your site content:
$ mkdir ~/www
$ echo "<h1>hello world</h1>" > ~/www/index.html
# pkg install -y nginx
# service nginx enable
# sed -i .bak 's#root.*/usr/local/www/nginx;#root /home/patmaddox/www;#' /usr/local/etc/nginx/nginx.conf
# service nginx start
At this point, you can access your site via any accessible IPs (the tailscale IP, localhost, and the host’s network).
Let’s configure the host to only accept HTTP traffic on the Tailscale interface, using pf.
Lock down the server with a firewall
Note: Make sure you set the ext_if variable to match your interface name, otherwise you will lock yourself out!
Configure the firewall:
# sh # switch to sh to get real heredoc support
# cat > /etc/pf.conf << 'EOF'
ext_if="vtnet0"
ts_if="tailscale0"
set skip on lo
scrub in
block in
pass out
pass in proto tcp to port { 22 }
pass in inet proto icmp icmp-type { echoreq }
pass in on $ts_if proto tcp to port { 80 }
EOF
# service pf enable
# service pf start
At this point, your connection will hang. SSH to your host again.
You will now be able to access the web server via the Tailscale IP, and the loopback address, but not any other IPs.
Listen only on the Tailscale IP
Although pf prevents outsiders from accessing port 80, nginx is still listening on all interfaces:
# sockstat -l | grep nginx
www nginx 2765 6 tcp4 *:80 *:*
root nginx 2764 6 tcp4 *:80 *:*
If I mess up the firewall configuration at some point, my site may become visible on other interfaces.
Configure nginx to listen only on the Tailscale IP:
# sed -i .bak "s/listen.*80;/listen `tailscale ip -4`:80;/" /usr/local/etc/nginx/nginx.conf
# service nginx reload
# sockstat -l | grep nginx
www nginx 2765 6 tcp4 100.100.100.100:80 *:*
root nginx 2764 6 tcp4 100.100.100.100:80 *:*
Now the only way to access this web server is via another host on my Tailnet. That’s pretty locked down!
We can take it a step further by serving HTTPS instead of HTTP, using Tailscale’s built-in Let’s Encrypt certificates.
Configure HTTPS with Tailscale certs
Generate certificates:
# mkdir /usr/local/etc/nginx/certs
# tailscale cert \
--cert-file /usr/local/etc/nginx/certs/`hostname`.crt \
--key-file /usr/local/etc/nginx/certs/`hostname`.key \
<full-tailscale-hostname>
Now edit /usr/local/etc/nginx/nginx.conf
to point to the cert files, and change the listener for HTTPS:
listen 100.100.100.100:443 ssl;
server_name <full-tailscale-hostname>;
ssl_certificate /usr/local/etc/nginx/certs/<hostname>.crt;
ssl_certificate_key /usr/local/etc/nginx/certs/<hostname>.key;
Open port 443 on the firewall, and reload nginx and pf
# sed -i .bak 's/80/443/' /etc/pf.conf
# service nginx reload
# service pf reload
There you have it: a private website, accessible only via Wireguard-encrypted tunnel, with host verification.