Onionboat: Using Docker for easy Tor hidden services

Recently I wrapped up the technical work on a community hosting platform I've maintained for a number of years (Chambana.net). One of the tools I used in order to do that was Docker, and for better or worse I became relatively familiar with the (many) ins and outs of that tool. While I struggled with some of the clunky pieces of Docker's implementation of containers (for instance, it seems like they've only recently put much thought into the idea that people might actually want to actually store data), it clearly has the kind of momentum that only comes from being the trendiest solution on the block, and consequently lots of people have used it to build a lot of really cool stuff.

One of those cool things is docker-gen, a small templating utility. Essentially, docker-gen listens on the docker daemon's control socket, and based on signals it gets about containers starting or stopping, it rewrites configuration files based on Go templating syntax. The upshot of this is that docker-gen is used in other containers like nginx-proxy and letsencrypt-nginx-proxy-companion to automatically configure nginx to proxy connections to all of the web services you run on a host while also pulling down certificates from Letsencrypt. Fantastically useful, doubly so when you are trying to host a lot of microservices on a single IP address.

Based on that experience, I started thinking about other situations where you might want to configure a bunch of services behind an autoconfiguring proxy, and especially situations that could benefit from the kinds of isolation that containers could provide. One obvious use case was for Tor hidden services.

I'm not going to get into explaining too much about Tor. Basically it's a sophisticated network for anonymizing your connection to the internet, and is one of the most powerful tools for the protection of free speech online. But besides just anonymizing individual internet connections, Tor also allows for hosting "hidden services." These can be almost any kind of network service, but instead of a standard URL they are made available over the Tor network at a .onion address. Websites and other services accessed that way also have their location anonymized, making them takedown-resistant.

However, even though Tor may anonymize where these services are located on the internet, the services themselves may leak all kinds of information if they are misconfigured. Misconfigured web servers are one of the biggest causes of de-anonymization of Tor hidden services. A misconfigured web server may, for instance, leak its real IP address through headers or status pages.

One solution to this problem is to isolate the services themselves, so that they don't have any information about their local network environment to leak. Docker supports interconnecting containers through one or more virtual networks, and so I thought to experiment with how this might be applied to Tor hidden service isolation.

Enter Onionboat. I created Onionboat as a take on the kind of thing that the aforementioned nginx-proxy does, but applied to hidden services. Onionboat is a Docker container that not only installs and runs Tor, but will automatically configure Tor to expose other Docker containers as hidden services. Warning: this solution hasn't been security audited, and is posted as a fun experiment. The overall security of Docker containers and isolation of their networks is likely to vary based on your Linux distribution.

To try it (assuming you have Docker installed on your Linux box; I haven't tried it on other systems), just run the Onionboat container:

docker run --name onionboat -d -p 9001:9001 -v /var/run/docker.sock:/tmp/docker.sock:ro jheretic/onionboat

This will download the Onionboat image and create a running container instance from it called onionboat. Now you'll want to create an isolated network that doesn't have access to the internet for your services to reside in:

docker network create -o "com.docker.network.bridge.enable_ip_masquerade=false" faraday

This created a network called faraday that has IP masquerading disabled, meaning it won't be able to access the internet. Now attach faraday to the running onionboat container:

docker network connect faraday onionboat

Now the onionboat container is connected to two networks: the default docker bridge providing internet connectivity, and the isolated faraday network where we'll start our services. For the purposes of this tutorial, we'll use the stock nginx image just so it's easy to see it working. Start an nginx instance connected to the faraday network with a special environment variable:

docker run -d --net faraday -e HIDDENSERVICE_NAME=nginx nginx

The environment variable HIDDENSERVICE_NAME is read by docker-gen running in the onionboat container, which according to its template identifies it as something that should be added to the list of hidden services in torrc. It automatically uses whichever port is exposed by the container by default; if there's more than one, it uses port 80 by default but that behavior can be overridden by specifying a HIDDENSERVICE_PORT environment variable as well. Containers specifying the same HIDDENSERVICE_NAME are added to the same service. In this way, you can have multiple different containers providing services on different ports of the same .onion address. To see if it worked and to find out what .onion address was assigned, you can execute the following command:

docker exec onionboat cat /var/lib/tor/hidden_services/<HIDDENSERVICE_NAME>/hostname

Where <HIDDENSERVICE_NAME> is replaced with the name you provided in the environment variable. This should print out a long hash followed by .onion, for instance (a fake example):

m44Fr7zzjYuDvQmQfvwXRhCS.onion

Now if you open the Tor browser and paste the .onion address into the address bar, you should see the default nginx page:

default_page

Awesome! But the best part is that the nginx container can't access the internet, so it's much harder for it to leak network data! One caveat for this method is that it will only work for services that do not need to connect out to the internet, in an anonymized fashion or otherwise. If you're running a service that needs access to the internet over the Tor network, you'll need to either configure your service so that it proxies its connection over Tor, or you can look into some experimental work on a custom Tor network driver for Docker.

Many thanks to Jason Wilder, whose docker-gen utility does most of the heavy lifting here, and as always to the Tor project.

Josh King

Josh King is a largely self-taught developer, activist, and hacker with a focus on social justice.