← back

Onions Last updated: 2021-12-29, written in EN 🇬🇧

How to setup a Tor hidden service with a custom address on Debian with Nginx, MariaDB, PHP (LEMP) + phpMyAdmin and SFTP access

Tor hidden services allow various types of services (web server, telnet server, chat server, etc) to be operated within the Tor network. This allows both users and service operators to conceal their identities and locations. Just about anything that can be run on the clearnet can be run within the Tor deepweb.

Setting up a hidden service on Tor is a simple process and depending on the level of detail, an operator can keep their service completely anonymous. Depending on your use-case, you may or may not choose to anonymize your service at all. For anonymous operation, it is recommended to bind services being offered to localhost and make sure that they do not leak information such as an IP address or hostname in any situation (such as with error messages).

For this guide, we assuming a Debian "Buster" system.

It is recommended to use an instance in a VirtualBox, so that you don't litter your main operating system and that you can start all over if something went wrong.

The following introduction is for educational purpose only.

  1. Installing Tor ↓
  2. Installing PHP ↓
  3. Installing and configuring MariaDB ↓
  4. Configuring the hidden service ↓
  5. Configure a web server with Nginx ↓
  6. Configuring phpMyAdmin ↓
  7. Setup SFTP access ↓
  8. (Optional) Generating a custom .onion address ↓

Installing Tor

Before configuring a relay, the Tor package must be set up on the system. While Debian does have a Tor package in the standard repositories, we will want to add the official Tor repositories and install from there to get the latest software and be able to verify its authenticity.

First, we will edit the sources list so that Debian will know about the official Tor repositories.

sudo nano /etc/apt/sources.list

At the bottom of the file, paste the following two lines and save/exit.

deb http://deb.torproject.org/torproject.org buster main deb-src http://deb.torproject.org/torproject.org buster main

Now back in the console, we will add the Tor Project’s GPG key used to sign the Tor packages. This will allow verification that the software we are installing has not been tampered with.

wget https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc apt-key add <A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc

Remove the key file:

rm -r A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc

Lastly, run an update and install Tor from the repositories we just added.

apt-get update apt-get install tor deb.torproject.org-keyring

Installing PHP

We'll install the dependencies and the 3rd party repository called Sury for PHP 8.

sudo apt install apt-transport-https lsb-release ca-certificates wget curl -y sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'

Adding the GPG key and updating the packages.

curl -sSL -o /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg sudo apt update

Now it is time to install PHP 8:

sudo apt install php8.0 php8.0-fpm php8.0-curl php8.0-mysqlnd php8.0-zip php8.0-mbstring php8.0-bcmath php8.0-gd php8.0-xml -y

Installing and configuring MariaDB

We are installing the required packages and adding the signing key to the system.

sudo apt install -y software-properties-common dirmngr sudo apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xF1656F24C74CD1D8

Now we are adding the MariaDB repository.

sudo add-apt-repository 'deb [arch=amd64] http://nyc2.mirrors.digitalocean.com/mariadb/repo/10.4/debian buster main'

Update the packages and install MariaDB

sudo apt update sudo apt install -y mariadb-server mariadb-client

Run sudo mysql_secure_installation command to do initial set up to secure MariaDB installation.

sudo mysql_secure_installation

We will now get asked different questions. Follow the instructions:

Enter current password for root (enter for none): Press Enter
Switch to unix_socket authentication [Y/n] N
Change the root password? [Y/n] Y
New password: Enter Password
Re-enter new password: Re-Enter Password
Remove anonymous users? [Y/n] Y
Disallow root login remotely? [Y/n] Y
Remove test database and access to it? [Y/n] Y
Reload privilege tables now? [Y/n] Y

Configuring the hidden service

We will be editing the torrc file, so let’s bring it up in our text editor:

sudo nano /etc/tor/torrc

Going line by line in this file is tedious, so to minimize confusion, we will ultimately rewrite the whole file. We will implement logging into a file located at /var/log/tor/notices.log and assume the local machine has a web server running on port 80. Paste the following over the existing contents in your torrc file:

Log notice file /var/log/tor/notices.log HiddenServiceDir /var/lib/tor/my_service/ HiddenServicePort 80 HiddenServicePort 81 HiddenServicePort 22

After saving the file, make and permission a log file, then we are ready to restart Tor:

sudo touch /var/log/tor/notices.log chown debian-tor:debian-tor /var/log/tor/notices.log sudo service tor restart

If the restart was successful, the Tor hidden service is active. If not, be sure to check the log file for hints as to the failure:

sudo nano /var/log/tor/notices.log

There is a hostname file at /var/lib/tor/my_service/hostname that contains the hidden service’s public key. This public key acts as a .onion address which users on the Tor network can use to access your service. Make a note of this address after reading it from the file with cat:

sudo cat /var/lib/tor/my_service/hostname

Output: qwr...pid.onion

There is also a private_key file that contains the hidden service’s private key. This private key pairs with the service’s public key. It should not be known or read by anyone or anything except Tor, otherwise someone else will be able to impersonate the hidden service. If you need to move your Tor hidden service for any reason, make sure to backup the hostname and private_key files before restoring them on a new machine.

After restarting the hidden service, it may not be available right away. It can take a few minutes before the .onion address resolves on a client machine.

Configure a web server with Nginx

Let's use this hidden service to host a website with Nginx.

First, we will install Nginx and create a directory for our HTML files

sudo apt-get install nginx sudo mkdir -p /var/www/hidden_service/

Now, we will create an HTML file to serve, so we need to bring one up in our editor:

sudo nano /var/www/hidden_service/index.html

Paste the following basic HTML and save it:

<html><head><title>Hidden Service</title></head><body><h1>It works!</h1></body></html>

Next, we will set the owner of the files we created to www-data for the web server and change the permissions on the /var/www directory.

sudo chown -R www-data:www-data /var/www/hidden_service/ sudo chmod -R 755 /var/www

We want to make some configuration changes for anonymity. First, let's edit the default server block:

sudo nano /etc/nginx/sites-available/default

Find the block that starts with server { and you should see a line below that reads listen 80 default_server;. Replace this line with to explicitly listen on localhost:

listen localhost:80 default_server;

Now find the line in the block for server_name set the server name explicitly:

server_name _;

Next we need to edit the Nginx configuration file:

sudo nano /etc/nginx/nginx.conf

Find the block that starts with http { and set the following options:

keepalive_timeout 300; server_names_hash_bucket_size 128; server_name_in_redirect off; server_tokens off; port_in_redirect off;

The first option will keep the server response time up to 5 minutes, which can be useful on slow connections. The second option is necessary so the server can resolve the long vanity v3 domain name. The third option will make sure the server name isn't used in any redirects. The forth option removes server information in error pages and headers. The fifth option will make sure the port number Nginx listens on will not be included when generating a redirect.

Now we need to create a server block so Nginx knows which directory to serve content from when our hidden service is accessed. Using our text editor, we will create a new server block file:

sudo nano /etc/nginx/sites-available/hidden_service

In the empty file, paste the following configuration block. Make sure that the server_name field contains your onion address which you read from the hostname file earlier and not my address, qwr...pid.onion.

server { listen; server_name qwr...pid.onion; error_log /var/log/nginx/hidden_service.error.log; access_log off; root /var/www/hidden_service; index index.php index.html index.htm; server_tokens off; location / { allow; deny all; try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/var/run/php/php8.0-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }

After saving the file, we need to symlink it to the sites-enabled directory and then restart Nginx:

sudo ln -s /etc/nginx/sites-available/hidden_service /etc/nginx/sites-enabled/hidden_service sudo service nginx restart

To test the hidden service, download and install the Tor Browser on any machine and load up your .onion address.

Configuring phpMyAdmin

Install PHP extensions for phpMyAdmin to connect with the database.

sudo apt install -y php-json php-mbstring php-xml

The phpMyAdmin package is now available in the Debian repository. But, we do not use it here since it is an older version. So, we will download the latest version from the official website.

wget https://files.phpmyadmin.net/phpMyAdmin/5.1.1/phpMyAdmin-5.1.1-all-languages.tar.gz

Extract phpMyAdmin using the tar command.

tar -zxvf phpMyAdmin-5.1.1-all-languages.tar.gz

Move the phpMyAdmin to your desired location.

sudo mv phpMyAdmin-5.1.1-all-languages /usr/share/phpMyAdmin

Copy the sample configuration file.

sudo cp -pr /usr/share/phpMyAdmin/config.sample.inc.php /usr/share/phpMyAdmin/config.inc.php

Edit the configuration file.

sudo nano /usr/share/phpMyAdmin/config.inc.php

Generate a blowfish secret and update the secret in the configuration file. Use your own secret!

$cfg['blowfish_secret'] = 'DjnEodjS37saeWAMg6sdeOPQX8RzeV8x'; /* YOU MUST FILL IN THIS FOR COOKIE AUTH! */

Also, uncomment the phpMyAdmin storage database and tables.

$cfg['Servers'][$i]['pmadb'] = 'phpmyadmin'; $cfg['Servers'][$i]['bookmarktable'] = 'pma__bookmark'; $cfg['Servers'][$i]['relation'] = 'pma__relation'; $cfg['Servers'][$i]['table_info'] = 'pma__table_info'; $cfg['Servers'][$i]['table_coords'] = 'pma__table_coords'; $cfg['Servers'][$i]['pdf_pages'] = 'pma__pdf_pages'; $cfg['Servers'][$i]['column_info'] = 'pma__column_info'; $cfg['Servers'][$i]['history'] = 'pma__history'; $cfg['Servers'][$i]['table_uiprefs'] = 'pma__table_uiprefs'; $cfg['Servers'][$i]['tracking'] = 'pma__tracking'; $cfg['Servers'][$i]['userconfig'] = 'pma__userconfig'; $cfg['Servers'][$i]['recent'] = 'pma__recent'; $cfg['Servers'][$i]['favorite'] = 'pma__favorite'; $cfg['Servers'][$i]['users'] = 'pma__users'; $cfg['Servers'][$i]['usergroups'] = 'pma__usergroups'; $cfg['Servers'][$i]['navigationhiding'] = 'pma__navigationhiding'; $cfg['Servers'][$i]['savedsearches'] = 'pma__savedsearches'; $cfg['Servers'][$i]['central_columns'] = 'pma__central_columns'; $cfg['Servers'][$i]['designer_settings'] = 'pma__designer_settings'; $cfg['Servers'][$i]['export_templates'] = 'pma__export_templates';

Import the create_tables.sql to create tables for phpMyAdmin. Enter your MariaDB password for root.

sudo mysql < /usr/share/phpMyAdmin/sql/create_tables.sql -u root -p

Create a virtual host configuration file for phpMyAdmin under the /etc/nginx/conf.d directory.

sudo nano /etc/nginx/conf.d/phpMyAdmin.conf

Use the following information to create a virtual host for phpMyAdmin. Change the server_name as per your requirement.

server { listen 81; server_name qwr...pid.onion; root /usr/share/phpMyAdmin; location / { index index.php; auth_basic "Admin Area"; auth_basic_user_file /etc/apache2/.htpasswd; } ## Images and static content is treated different location ~* ^.+.(jpg|jpeg|gif|css|png|js|ico|xml)$ { access_log off; expires 30d; } location ~ /\.ht { deny all; } location ~ /(libraries|setup/frames|setup/libs) { deny all; return 404; } location ~ \.php$ { include /etc/nginx/fastcgi_params; fastcgi_pass unix:/run/php/php8.0-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /usr/share/phpMyAdmin$fastcgi_script_name; } }

Create a tmp directory for phpMyAdmin and then change the permission.

sudo mkdir /usr/share/phpMyAdmin/tmp sudo chmod 777 /usr/share/phpMyAdmin/tmp

Set the ownership of the phpMyAdmin directory.

sudo chown -R www-data:www-data /usr/share/phpMyAdmin

Now we will build another protection layer for the web interface using a .htpasswd-File.

First we have to make sure that we have the right tools installed.

sudo apt install apache2-utils

Now we create a password file and a first user. Run the htpasswd utility with the -c flag (to create a new file), the file pathname as the first argument, and the username as the second argument:

sudo htpasswd -c /etc/apache2/.htpasswd user1

Press Enter and type the password for user1 at the prompts.

Optionally: Create additional user-password pairs. Omit the -c flag because the file already exists:

sudo htpasswd /etc/apache2/.htpasswd user2

You can confirm that the file contains paired usernames and hashed passwords:

cat /etc/apache2/.htpasswd



Restart the services.

sudo systemctl restart nginx sudo systemctl restart php8.0-fpm

You can access the phpMyAdmin web interface by going to the domain with an ending :81 like so qwr...pid.onion:81

Put in your .htpasswd credentials and than your MariaDB root credentials.

Setup SFTP access

SFTP is built upon the SSH transport layer and should be installed on most Linux server distributions by default. If it isn’t, you can install with:

sudo apt install ssh

Then change the Subsystem to internal-sftp in sshd_config.

sudo nano /etc/ssh/sshd_config

Scroll to the bottom of the file and comment out the line Subsystem sftp by adding # before it and then add Subsystem sftp internal-sftp below it.

#Subsystem sftp /usr/lib/openssh/sftp-server Subsystem sftp internal-sftp

This tells sshd to use SFTP server code built into sshd instead of running sftp-server, which is now redundant and only kept for a backward compatibility.

Restart the sshd service for changes to take affect.

sudo service sshd restart

Now we have to create a SFTP-User.

It’s not recommended that you use the root account or any account with sudo privileges to upload files to the web server document root. For this reason, you should create a new user that only has SFTP access to the document root.

In this guide, we are calling the SFTP user sftpuser – you can call this whatever you want. If you plan to give SFTP access to multiple users across different document roots, consider a naming scheme like username_domain_com. For example admin_dennisosaj_de. This will make it easier to keep track of all your SFTP users.

For the purposes of this guide, we will name the SFTP user sftpuser.

sudo adduser sftpuser

Generate a password and press enter to accept all defaults.

Next we will create a Match User directive in the SSH config and add your SFTP user to the www-data group.

Restrict the user sftpuser to the document root and also disable their SSH access – we only want them to be able to log in over SFTP. We can do this by adding a Match User directive in the SSH config file.

Begin by opening sshd_config.

sudo nano /etc/ssh/sshd_config

Scroll down to the bottom of the SSH config file and add your new Match User directive.

Make sure that ChrootDirectory is the directory above your document root. For example, this guide's document root is /var/www/hidden_service/, then the ChrootDirectory is /var/www/.

Note you can add multiple users here separated by a comma, e.g. Match User sftpuser, sftpuser2, sftpuser3

Note: ForceCommand internal-sftp will force SFTP access only and not allow this SFTP user to log in via SSH.

Match User sftpuser ChrootDirectory /var/www/ ForceCommand internal-sftp X11Forwarding no AllowTcpForwarding no PasswordAuthentication yes

Test SSH config before restarting.

sudo sshd -t

If no errors, restart the sshd service for changes to take affect.

sudo service sshd restart

Next we have to add the SFTP user sftpuser to the www-data group.

sudo usermod -a -G www-data sftpuser

Note: Linux groups do not take affect until the user logs out and in again. If you are already logged in as this user in your FTP client, close the program completely and then log in again.

If you need to provide other SFTP users write access to the document root, simply add their usernames separated by a comma, e.g. Match User sftpuser, sftpuser2, sftpuser3 in sshd_config and then add the SFTP user to the www-data group.

Now you can login to your hidden service using SFTP. Make sure Tor is running on the machine from which you want to login and that the proxy settings are set in your FTP-client (e.g. FileZilla):

Generic Proxy: SOCKS 5
Proxy host:
Proxy port: 9150

(Optional) Generating a custom .onion address

We are using a tool called mkp224o. So first of all download the repository using git.

git clone https://github.com/cathugger/mkp224o.git

Afterwards run:

apt install gcc libsodium-dev make autoconf

After you successfully downloaded the repository and necessary packages go into the directory with:

cd mkp224o

Now build and init the tool.

./autogen.sh ./configure make

The generator needs one or more filters to work.

It makes directory with secret/public keys and hostname for each discovered service. By default root is current directory, but that can be overridden with -d switch.

Run it like ./mkp224o dennis, and it will try creating keys for onions starting with "dennis" in this example. To not litter current directory and put all discovered keys in directory named "keys" use:

./mkp224o -d keys dennis


set workdir: keys/
sorting filters... done.
in total, 1 filter
using 4 threads

You can use what ever letters you want, but stay under 5-6 letters, otherwise the search process will take to much computing power.

Wait some time and after it found one or multiple addresses, you can cancel the tool with ctrl + C.

Now we have to copy one of the generated folders with its content to where we want our service keys to reside. First go to the root directory.

cd ../ sudo cp -r mkp224o/keys/dennis...hjt.onion /var/lib/tor/custom

Replace dennis...hjt.onion with the full name of your directory which you created just now.

You may need to adjust ownership and permissions:

sudo chown -R debian-tor: /var/lib/tor/custom sudo chmod -R u+rwX,og-rwx /var/lib/tor/custom

Go into the Tor config file and replace HiddenServiceDir /var/lib/tor/my_service/.

sudo nano /etc/tor/torrc HiddenServiceDir /var/lib/tor/custom/

Now restart Tor to let it take effect.

sudo service tor restart

Next we have to change the server_name in the following files. Just replace the old .onion-address with the new one.

sudo nano /etc/nginx/sites-available/hidden_service sudo nano /etc/nginx/conf.d/phpMyAdmin.conf

Restart the Server and wait a minute to to let the changes take effect.

sudo service nginx restart

Your Server is reachable from your custom .onion-address now. Congrats!


Diese Seite verwendet Cookies.

Um die Seite optimal gestalten und fortlaufend verbessern zu können und zur statistischen Auswertung, verwenden wir Cookies. NĂ€here Informationen dazu und zu Ihren Rechten als Benutzer finden Sie in unserer DatenschutzerklĂ€rung. Klicken Sie auf „Ich stimme zu“, um Cookies zu akzeptieren und direkt unsere Website besuchen zu können, oder klicken Sie auf „Einstellungen anpassen“ (am Ende des Cookie-Hinweises), um Ihre Cookies selbst zu verwalten.

Ich stimme zu

Hinweis zur Verarbeitung deiner auf dieser Webseite erhobenen Daten in den USA durch Google, Facebook, LinkedIn, Twitter, Youtube: indem du auf “Ich stimme zu" klickst, willigst du zugleich dem. Art 49 Abs. 1 S. 1 lit. a DSGVO ein, dass deine Daten in den USA verarbeitet werden. Die USA werden vom europĂ€ischen Gerichtshof als ein Land mit einem nach EU-Standards unzureichendem Datenschutzniveau eingeschĂ€tzt. Es besteht insbesondere das Risiko, dass deine Daten durch US-Behörden, zu Kontroll- und zu Überwachungszwecken, möglicherweise auch ohne Rechtsbehelfsmöglichkeiten, verarbeitet werden können. Wenn du die verwendeten Cookies unter “Einstellungen anpassen” abwĂ€hlst, dann findet die vorgehend beschriebene Übermittlung nicht statt.

Essentielle Cookies

Cookie Settings
Speichert Einstellungen, die hier getroffen werden. (1 Tag bis 2 Jahr)


Google Analytics | Google LLC | DatenschutzerklÀrung
Cookie von Google fĂŒr Website-Analysen. Erzeugt statistische Daten darĂŒber, wie der Besucher die Website nutzt. Alle Informationen werden anonymisiert gespeichert. (2 Jahre)

Ich stimme zu
Emoji Pointer Up