Asselin.Engineer

Tailscale VPN in Docker Without Elevated Privileges

2022-08-22
ece web
Last Update: 2022-09-01

This post details how to add Tailscale to a docker-compose.yml without elevated privileges. It is useful for situations where you only want the actual container (not the entire host device) to be accessible in the VPN. To see the solution, skip to the solution section.

This might seem trivial but in early 2022, I did not find enough information on how to do this or if it was even possible. The closest I found is the Tailscale docs on userspace networking. The diagram above is slightly modified from the same page.

Context

Before sharing how this is achieved, here is a fictional situation to explain why.

I deploy most of my projects in containers which tend to end up running on a dedicated linux server. Most projects have a public website or API to visualize and debug results.

Sometimes, they are deployed on the edge, on a machine without a public IP and no sudo powers. All I can do is provide a Dockerfile or docker-compose.yml file. In that case, when the client wants to access their url publicly, it can be difficult.

One solution to this problem is to use an other server which has a public IP. This server runs a webserver like nginx and redirects requests coming from the domain name to the edge device. Both are configured to be in the same Virtual Private Network (VPN). Also, this allows me to debug the container easily since I can add my laptop to the VPN when I want.

The problem with this solution is that we don't have access to the edge device to install the VPN. We can only provide a Dockerfile or docker-compose.yml! Meaning the Dockerfile must contain the VPN configurations.

For this project, since many independent services had to be deployed on the same edge device, we use Docker Compose. The solution detailed below explains how to add tailscale to a multi-container setup using Tailscale. You can easily adapt the solution to a single Dockerfile.

Tailscale

Tailscale is a VPN service that is built on top of WireGuard. It offers a simplified UX to manage a VPN. It is really easy to use and greatly simplifies connecting new devices and users to your VPN. I am in no way associated to Tailscale. I just really enjoy using it.

Setting up Tailscale is super easy but in this case, it is not: we do not have sudo permissions. To keep things containerized, only the equivalent of docker run can be used and without --privileged or any other additionnal capabilities. For security reasons, we don't want the container to have access to the host devices.

Most instructions on how to use tailscale in docker assume you want to include the entire host network in the VPN (see docker image tailscale/tailscale). This requires elevated networking device privileges. All we want is access to our service.

An other solution is to use the new (2022) Tailscale Docker Desktop extension. This does not work in our case since Docker Desktop is for Windows or Mac and we use a linux machine. Read more in their blog post.

Solution

Add tailscale as a service in your docker-compose.yml file.

version: "3.9"
services:
  tailscale:
    build:
      context: .images/tailscale
    environment:
      - TAILSCALE_AUTH_KEY
  service-one:
    image: nginxdemos/hello
    network_mode: "service:tailscale"

To see the full example solution, go to the github repository lpasselin/tailscale-docker and look around.

Notice the network_mode being used for some-service. This means service-one reuses the same network as the tailscale service.

Also, the images/tailscale/start.sh script starts tailscale with tailscaled --tun=userspace-networking. Without this, additional networking capabilities are required. For more information, see the Tailscale docs page on userspace networking mode (for containers).

When adding a node, Tailscale adds a suffix to the hostname if it already exists. Even if the hostname is specified with the tailscaled --hostname=my-hostname command. There are two ways to solve this. The first method is to save the tailscale state to a persistent volume or local host folder. The stateful-example shows how to do this using a docker volume. The second method is used by all examples except the stateful example. It uses the tailscaled --state=mem: flag to save state to memory. When combined with an ephemeral auth_key, makes the tailscaled daemon perform the equivalent of tailscale logout on termination. This immediately removes the node from the network and allows future deployments to reuse the same hostname without suffix. This feature was released in version 2.30, which was published 3 days after initialy publishing this page! For more information, see the tailscale post.

Finally, if you absolutely want to use a single docker container, simply install tailscale in the same container. I suggest following the images/tailscale/start.sh script as a reference.

Contributions

Thanks for the PR and helpful emails:

  • rhjensen79 added the kubernetes example and a github action to automatically build and push the image.
  • Kim Frederiksen idea for stateful-example which saves the tailscale state to a docker volume.