Monday 16th September 2019
As part of an ongoing project to implement version control and configuration management for all aspects of my infrastructure, I have recently moved the hosting for my Tor Hidden Services onto my main web infrastructure, rather than using a separate dedicated machine. Both of my web servers are set up and managed entirely using Ansible, so I had to put together a new Ansible playbook to install and configure Tor. I've documented it here if anyone else may find it useful.
The configuration described in this article is intended for use on Debian/Ubuntu-based systems, however with minor modifications it should be usable on other systems as well.
The guide assumes that you already have an Ansible playbook set up, including a hosts file, and are able to connect via SSH to the intended server.
Skip to Section:
Deploying a Tor Onion v3 Hidden Service Using Ansible ┣━━ Installing Tor ┣━━ Configuring Tor ┣━━ Testing the Hidden Service ┗━━ Conclusion
If you are using a Debian system, a long-term support version of Tor is available in the default Apt repositories. However, to benefit from the latest features and security improvements, it is recommended to add the Tor repository to your Apt sources and install the latest stable version.
If you are using an Ubuntu system, you shouldn't use the tor
package from the default Apt repositories, as it is frequently out of date. This is not the fault of the Tor Project maintainers, but rather because the package is part of the 'Universe' repositories, meaning that the packages are community-maintained, rather than maintained by Canonical.
The Tor repositories are available over HTTPS, so it is recommended to install apt-transport-https
to allow Apt to download packages this way:
- name: "Install apt-transport-https" apt: update_cache: yes name: apt-transport-https
Next, you need to download and import the Tor package GPG signing key to Apt. Make sure to verify the key fingerprint and download location below:
- name: "Add Tor repo GPG signing key to Apt" apt_key: url: "https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc" id: A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89 state: present
The next step is to actually add the Tor repository to your Apt sources and update the Apt cache. If you're using a distribution other than bionic
, make sure to adjust the example to specify your own distribution name:
- name: "Add Tor repo to Apt sources" apt_repository: repo: "deb https://deb.torproject.org/torproject.org bionic main" update_cache: yes validate_certs: yes state: present
Finally, install the required packages for Tor - tor
and deb.torproject.org-keyring
, which will keep the package repository signing keys up to date:
- name: "Install Tor packages" apt: update_cache: yes name: "{{ tor_packages }}" vars: tor_packages: - tor - deb.torproject.org-keyring
Now that you've written the steps required for securely installing Tor, the next step is to configure it and set up your hidden service.
Firstly, you need to create and then set a torrc
file, which is the Tor configuration file. In your local Ansible directory, you can create the torrc
file, and then have this be uploaded to the server when you run the playbook.
Create the file on your local machine in a location that Ansible will be able to read. I personally prefer to create a files
directory in my local Ansible directory, and then recreate the file system structure of the remote server. For example, the file /etc/tor/torrc
on the remote machine would be stored in files/etc/tor/torrc
on the local machine:
HiddenServiceDir /var/lib/tor/v3_hidden_service/ HiddenServiceVersion 3 HiddenServicePort 80 127.0.0.1:80
You can configure the parameters in the file as required. The HiddenServicePort
configuration is used to forward traffic arriving at your hidden service on a particular port to a different destination, such as a local web server. In the sample above, connections to port 80 will be forwarded to 127.0.0.1 on port 80. It is not recommended to forward traffic outside of the local machine, as this has the potential to de-anonymize your hidden service.
Next, copy the torrc
file and set the required owner and permissions:
- name: "Set Torrc" copy: src: "files/etc/tor/torrc" dest: "/etc/tor/torrc" owner: root group: root mode: u=rw,g=r,o=r
The next two sections are only required if you wish to import the private key from an existing hidden service, e.g. if you want to use the same .onion
address instead of generating a new one.
Create the directory where your hidden service private key will be copied to:
- name: "Create Tor HS directory" file: path: /var/lib/tor/v3_hidden_service state: directory owner: debian-tor group: debian-tor mode: u=rwx,g=,o=
In order to securely copy your existing Tor hidden service private key onto the server, I recommend creating a separate secrets
directory in your local Ansible directory, and storing the private key(s) in there.
Make sure to adjust the file paths and names as required. You only need to copy the private key file - the public key and/or hostname files are not required:
- name: "Set Tor HS keys" copy: src: secrets/hs_ed25519_secret_key dest: /var/lib/tor/v3_hidden_service/hs_ed25519_secret_key owner: debian-tor group: debian-tor mode: u=rw,g=,o=
Finally, restart the Tor service to apply the configuration:
- name: "Restart Tor" systemd: name: tor state: restarted
The Ansible 'notify' functionality could have instead been used to automatically restart Tor when required, but I have instead opted to explicitly restart Tor. This is to ensure that the Tor service is not accidentally re-enabled at the end of the playbook if you decide to follow the additional step below.
By default, you can only announce a hidden service from one machine at a time, so if you are deploying this configuration to multiple remote hosts, you should disable Tor (including starting Tor at boot) on all but one of them:
- name: "Disable Tor on all hosts except host1" systemd: name: tor enabled: no state: stopped when: ansible_hostname != "host1"
Once you have run your Ansible playbook against the desired remote hosts, you can perform some basic tests to verify that the hidden service is working correctly.
Firstly, check your hidden service hostname. You can run the following command on the remote host to view the hostname file containing the .onion
address:
$ cat /var/lib/tor/v3_hidden_service/hostname
If the file isn't present, run sudo service tor status
to check whether the Tor service is running properly. If not, there may be an issue somewhere, so check the Tor logs (which should be /var/log/tor
, or included in /var/log/syslog
) for errors and double check all of your configuration.
Secondly, check that you can connect to the hidden service. If you're running a web server behind your hidden service, you can connect to it either by entering the .onion
address into the URL bar of the Tor Browser, or using the curl
utility to connect through a local TorSOCKS proxy server, which should be running if you have Tor or Tor Browser installed:
$ curl --socks5-hostname 127.0.0.1:9050 your-hidden-service.onion
If you're unable to connect to your hidden service, try restarting Tor both on the local and remote machines, as sometimes there is a slight delay in connecting to brand-new hidden services when using an already-established Tor connection.
This Ansible playbook configuration should hopefully make deploying Tor Hidden Services much easier and more time efficient. It vastly improves the reliability of the setup process, and makes it fully reproducible.
To take the process to another level, you could implement some form of automatic testing that will ensure that the hidden service is responding correctly. For example, this could be done by automatically connecting to the hidden service over Tor at the end of the playbook, and checking for a specific response.
If you have any questions or encounter any issues with this configuration, please feel free to get in touch.