If you’ve followed my blog or other notes, you’ll be well aware of my extensive use of Tailscale for nearly everything – securing access to remote servers and for connecting to services across my local network.
Tailscale DNS is great for assigning DNS names to individual hosts, but my approach (usually) involves using Docker containers, with which I run several services on a single host. It is then fiddly (or impossible? I’m not sure) to use a single Tailscale “machine” to expose multiple services on the same host nicely via the DNS system (system).
Early this year, Alex from Tailscale wrote a blog post describing how to nicely use Tailscale alongside Docker, such that each service can be treated as a separate machine for which DNS can be managed. Further, the inbuilt LetsEncrypt support allows each service to also be served with a valid TLS certificate.
I now use these “sidecars” for pretty much every service I run.
Using Tailscale Sidecars
I use the OAuth approach (rather than Auth Keys), and below is my rough setup.
Tailscale Configuration
Login to your Tailscale dashboard and visit your Access Controls page. Here, add in some config to allow you to use tags in your sidecars:
{
...
"tagOwners": {
"tag:containers": ["autogroup:member"],
},
...
}
On the OAuth clients page, generate a new OAuth client (e.g. with a description of “sidecars”). Select “Read” and “Write” for the “Devices” scope. Add the tag we created earlier (“containers” above) using the dropdown. Click “Generate client”.
Make a note of the client ID and secret (be sure to keep the secret very safe).
Docker Configuration
We’re now ready to connect a service using a sidecar. In my example I’ll use Vaultwarden, but the same should apply mostly to any service running in Docker.
In the directory for the service (i.e. the directory containing the docker-compose.yml
file) create a new directory called tailscale-config
. In this directory, create a new file called ts.json
with the following content:
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"${TS_CERT_DOMAIN}:443": {
"Handlers": {
"/": {
"Proxy": "http://127.0.0.1:80"
}
}
}
},
"AllowFunnel": {
"${TS_CERT_DOMAIN}:443": false
}
}
The file above tells Tailscale, when running, to expose port 80 on the service as HTTPS on port 443, and to proxy requests to the service on port 80. If your service uses a different port, modify the ts.json
file accordingly (e.g. some services also comprise a database as well as a web service, and you want to make sure the web service is targeted).
Next, modify the docker-compose.yml
file such that it looks like the following:
services:
vaultwarden:
image: vaultwarden/server:latest
restart: unless-stopped
volumes:
- ./vaultwarden-data:/data
network_mode: service:tailscale-sidecar
tailscale-sidecar:
image: tailscale/tailscale:latest
hostname: vaultwarden
environment:
- TS_AUTHKEY=tskey-client-ABC123
- TS_EXTRA_ARGS=--advertise-tags=tag:containers
- TS_SERVE_CONFIG=/config/ts.json
- TS_STATE_DIR=/var/lib/tailscale
volumes:
- ./tailscale-data:/var/lib/tailscale
- ./tailscale-config:/config
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
restart: unless-stopped
Replace the TS_AUTHKEY
value with the client secret you generated earlier. Replace the hostname
value with the DNS name you want to use for the service (which will be the subdomain of your tailnet the service will be available at).
Bring up the service with docker compose up -d
and give it a minute or so to set up the DNS and provision certificates. Then (in this case) you could visit https://vaultwarden.tailnet-domain.ts.net to view the service.
It’s now much easier (and more secure) to expose and access the services across your tailnet, and from any of your devices.