Deploying a Tor Onion v3 Hidden Service Using Ansible

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

Installing Tor

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"
    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"
    url: ""
    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"
    repo: "deb bionic main"
    update_cache: yes
    validate_certs: yes
    state: present

Finally, install the required packages for Tor - tor and, which will keep the package repository signing keys up to date:

- name: "Install Tor packages"
    update_cache: yes
    name: "{{ tor_packages }}"
    - tor

Configuring Tor

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

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 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"
    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"
    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.


If you are storing your Ansible configuration and playbooks in a version control system such as Git, make sure to consider whether it is appropriate for your Tor Hidden Service private key(s) to be checked in. It is recommended to exclude these files, for example using .gitignore. Access to these files will allow someone to fully impersonate your hidden service, putting yourself and your users at risk, and requiring the keys to be changed, meaning that you'll lose your .onion address and have to switch to a new one.

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"
    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"
    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"
    name: tor
    enabled: no
    state: stopped
  when: ansible_hostname != "host1"

Testing the Hidden Service

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 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.

This article is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.