This article will describe how to point a .onion
domain at your existing wordpress sites (on wordpress multisite) so that your website will be accessible both on the clearnet and directly on the darknet via a .onion
domain.
Intro
There are numerous security benefits for why millions of people use tor every day. Besides the obvious privacy benefits for journalists, activists, cancer patients, etc — Tor has a fundamentally different approach to encryption (read: it’s more secure).
Instead of using the untrustworthy X.509 PKI model, all connections to a v3 .onion address is made to a single pinned certificate that is directly correlated to the domain itself (the domain is just a hash of the public key + some metadata).
Moreover, some of the most secure operating systems send all the user’s Internet traffic through the Tor network — for the ultimate data security & privacy of its users.
In short, your users are much safer communicating to your site using a .onion
domain than its clearnet domain.
For all these reasons, I wanted to make all my wordpress sites directly available to tor users. Unfortunately, I found that it’s not especially easy to point a .onion
domain at existing wordpress sites. There’s several “gotchas” to be aware of.
Fortunately for you, I’ve ironed-out all the kinks and documented them here, so you can painlessly make your wordpress sites accessible over a .onion
domain 🙂
Assumptions
Versions
At the time of writing, this guide was tested against the following software and versions:
- Debian 10
- Apache 2.4
- Tor 0.3.5.10
- WordPress Multisite 5.4.1
- Mercator 1.0.3
If you’re using a different OS, web server, or wordpress (plugin) version, then adaptations to the below commands may be necessary.
Document Root
This guide assumes that your wordpress document root is located at the following directory
/var/www/html/wordpress/htdocs/
If your wordpress multisite install is located in a distinct directory, then adaptations to the below commands will be necessary.
Root Access Required
This guide also assumes that you have root access to your server, which will be required to:
- Install & configure system-level packages (eg tor) and
- Modify your webserver config
Port 8050 is unused
This guide sets-up a new vhost for serving your website on 127.0.0.1:8050
(just for tor clients). If you already use port 8050, then you should substitute ‘8050
‘ in this guide with some other, unused port.
Not for Hidden Services
Finally, this guide assumes that you’re running a Onion Service that’s not hidden — since your existing websites are already accessible on the clearnet, we’re only adding a .onion
domain for the security benefits of our users.
Install Prereqs
Before updating your website to handle traffic from a new .onion
domain, you first need to make some changes to your server.
Install Tor
Tor is remarkably easy to install and setup.
sudo apt-get install tor
Move the default tor configuration file into a backup and write-out this new tor config.
mv /etc/tor/torrc /etc/tor/torrc.orig cat > /etc/tor/torrc <<'EOF' RunAsDaemon 1 DataDirectory /var/lib/tor HiddenServiceDir /var/lib/tor/onion_domain_1 HiddenServicePort 80 127.0.0.1:8050 EOF chown root:root /etc/tor/torrc chmod 0644 /etc/tor/torrc
The above config will cause tor to create a new .onion
domain (actually, generate a fresh public-private keypair that cooresponds to a new .onion
domain) and store the files (keys + metadata + authorized_clients) related to this domain in /var/lib/tor/onion_domain_1
.
The next line tells tor to send clients that connect on this new .onion
domain on port 80 to port 8050 on the localhost.
Below we’ll setup a vhost in our web server that will listen on 127.0.0.1:8050
for serving our tor clients.
⚠ Note: Because
.onion
domains are tied to a cryptographic keypair, you cannot simply choose your own domain.If you’d like a “vanity” domain for a v3 onion address, you can try to generate
.onion
addresses (keypairs) over-and-over until you find one that meets some regex requirements using a tool such as mkp224o
After the above changes, you should restart tor to generate and output your new .onion
address
systemctl restart tor cat /var/lib/tor/onion_domain_1/hostname
An example execution can be found below. Note that there is no ‘onion_domain_1
‘ directory until after tor is restarted.
root@host:~# ls /var/lib/tor cached-certs cached-microdesc-consensus cached-microdescs.new keys lock state root@host:~# root@host:~# systemctl restart tor root@host:~# root@host:~# ls /var/lib/tor cached-certs cached-microdescs keys onion_domain_1 cached-microdesc-consensus cached-microdescs.new lock state root@host:~# root@host:~# ls /var/lib/tor/onion_domain_1/ authorized_clients hostname hs_ed25519_public_key hs_ed25519_secret_key root@host:~# root@host:~# cat /var/lib/tor/onion_domain_1/hostname m7qd5n2qcdxnsduwglff3k2moctrvodtqgzag3n6isrp7xj7v3nhzmad.onion root@host:~#
As you can see from the contents of our ‘hostname
‘ file in the ‘/var/lib/tor/onion_domain_1
‘ directory above, our new .onion
domain is:
m7qd5n2qcdxnsduwglff3k2moctrvodtqgzag3n6isrp7xj7v3nhzmad.onion
Install Mercator
Mercator is the magical, low-level wordpress plugin that maps our ‘.onion
‘ domain to our desired multisite blog (blog_id
) via the ‘domain_mapping
‘ table.
Because of the low-level nature of the way that Mercator works, the install is a bit more complicated than most wordpress plugins.
Download the latest release tarball from their github page. Extract it, and move its contents into your wordpress wp-content/mu-plugins/mercator/
directory.
user@host:~$ wget -qO mercator.tar.gz $(curl -s https://api.github.com/repos/humanmade/Mercator/releases/latest | grep -i tarball_url | cut -d\" -f4) user@host:~$ user@host:~$ ls mercator.tar.gz user@host:~$ user@host:~$ tar -xzvf mercator.tar.gz humanmade-Mercator-21b0e83/ humanmade-Mercator-21b0e83/LICENSE.txt humanmade-Mercator-21b0e83/README.md humanmade-Mercator-21b0e83/admin.php humanmade-Mercator-21b0e83/class-mapping.php humanmade-Mercator-21b0e83/class-network-mapping.php humanmade-Mercator-21b0e83/composer.json humanmade-Mercator-21b0e83/inc/ humanmade-Mercator-21b0e83/inc/admin/ humanmade-Mercator-21b0e83/inc/admin/class-alias-list-table.php humanmade-Mercator-21b0e83/inc/cli/ humanmade-Mercator-21b0e83/inc/cli/class-mapping-command.php humanmade-Mercator-21b0e83/inc/cli/class-network-mapping-command.php humanmade-Mercator-21b0e83/mercator.php humanmade-Mercator-21b0e83/multinetwork.php humanmade-Mercator-21b0e83/sso-multinetwork.php humanmade-Mercator-21b0e83/sso.php user@host:~$ user@host:~$ [ ! -d /var/www/html/wordpress/htdocs/wp-content/mu-plugins ] && mkdir /var/www/html/wordpress/htdocs/wp-content/mu-plugins user@host:~$ user@host:~$ mv humanmade-Mercator-* /var/www/html/wordpress/htdocs/wp-content/mu-plugins/mercator user@host:~$
To finish the install of mercator, we have to setup the ‘wp-content/sunrise.php
‘ file. This is a special file that lets the mercator run very early (sunrise) so that it can set the site based on the domain that would otherwise be set before normal plugins are called.
ⓘ For more information on the sunrise file, see the ‘
wp-includes/ms-settings.php
‘ file thatinclude()
s it.
cd /var/www/html/wordpress/htdocs/wp-content/ cat > sunrise.php <<'EOF' <?php ################################################################################ # Author: Michael Altfield <michael@michaelaltfield.net> # Created: 2021-01-04 # Updated: 2021-01-04 # Version: 0.1 # Purpose: setup mercator for .onion site aliases. For more info, see: # * https://tech.michaelaltfield.net/2021/02/12/wordpress-multisite-tor/ # * https://github.com/humanmade/Mercator ################################################################################ // Default mu-plugins directory if you haven't set it defined( 'WPMU_PLUGIN_DIR' ) or define( 'WPMU_PLUGIN_DIR', WP_CONTENT_DIR . '/mu-plugins' ); # https://github.com/humanmade/Mercator/issues/110 add_filter( 'mercator.sso.enabled', '__return_false' ); add_filter( 'mercator.sso.multinetwork.enabled', '__return_false' ); require WPMU_PLUGIN_DIR . '/mercator/mercator.php'; ?> EOF
The above also disables the mercator sso, which I found necessary (yet undocumented) to prevent the following error:
The constant
COOKIE_DOMAIN
is defined (probably inwp-config.php
). Please remove or comment out thatdefine()
line.
Unfortunately, when I commented-out the COOKIE_DOMAIN
definition in wp-config.php
, it lead to the following error:
Error: Cookies are blocked or not supported by your browser. You must enable cookies to use WordPress.
The file responsible for the first error is the mercator/sso.php
file, and I found the fix in issue #110 on their github.
wp-config.php
First, to enable wordpress to call the ‘sunrise.php
‘ file, you must define()
the SUNRISE
constant in ‘wp-config.php
‘
# Enable the sunrise.php script (mercator) for .onion site aliases. # For more info, see: # * https://tech.michaelaltfield.net/2021/02/12/wordpress-multisite-tor/ # * https://github.com/humanmade/Mercator define('SUNRISE', true);
Now, because tor has built-in encryption that’s actually superior to the traditional X.509 PKI model of traditional tls-encrypted websites on the clearnet, your webserver can actually serve-back the content using the http://
scheme (as opposed to https://
).
However, depending on your server’s architecture and wordpress config, it may be non-trivial to convince wordpress to use http://
instead of https://
.
One way that I tell wordpress not to use https is by adding the following regex match to the top of my wp-config.php
file, and overriding some internal constants. We also set a new constant ONION_DOMAIN
— which we can use in our child theme’s functions later.
# Disable https for .onion site aliases. For more info, see: # * https://tech.michaelaltfield.net/2021/02/12/wordpress-multisite-tor/ if( preg_match( "/^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.)?[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.onion$/", $_SERVER['SERVER_NAME'] ) ){ define( 'ONION_DOMAIN', $_SERVER['SERVER_NAME'] ); define( 'WP_SITEURL', 'http://' .ONION_DOMAIN. "/" ); define( 'WP_HOME', 'http://' .ONION_DOMAIN. "/" ); define( 'WP_CONTENT_URL', 'http://' .ONION_DOMAIN. "/wp-content" ); define('FORCE_SSL_LOGIN', false); define('FORCE_SSL_ADMIN', false); unset( $_SERVER['HTTPS'] ); # might be needed to prevent infinite redirects or "Sorry, you are not allowed to access this page" errors } else if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'){ $_SERVER['HTTPS'] = 'on'; }
I actually found that the most difficult part of making my wordpress sites accessible over a .onion
domain was breaking all the http -> https
redirects that I had setup everywhere. Indeed, I think it would have been easier to just serve my sites over the .onion
domain using https — if only I could get a free cert for my .onion
domain from Let’s Encrypt.
As of 2020-02, it is not possible to get a cert for a .onion
domain from Let’s Encrypt, but there is a ticket open to address this. Unfortunately, the team stated that they aren’t sure if this feature request has sufficient demand to justify “allocating resources” to it.
If you, like me, expereinced issues bringing your clearnet site onto the darknet because of issues serving over http://
, then please give this ticket a +1
Add .onion Domain
Now that our prereqs are out of the way, let’s add our ‘.onion
‘ domain into our relevant configs. Per our previous section, we’ll use the following .onion
domain — but be sure to replace this with your own.
m7qd5n2qcdxnsduwglff3k2moctrvodtqgzag3n6isrp7xj7v3nhzmad.onion
Web Server
Because we never want our clearnet wordpress site to serve over http://
(but we need our .onion
to only serve over http://
), we need to create a new vhost that listens only on the localhost on some unused port (in this case we’ll arbitrarily pick port 8050).
Copy your existing vhost file and update:
ServerName
ServerAlias
, if applicableListen
- The
VirtualHost
bind address:port
Listen 127.0.0.1:8050 <VirtualHost 127.0.0.1:8050> ServerName m7qd5n2qcdxnsduwglff3k2moctrvodtqgzag3n6isrp7xj7v3nhzmad.onion ServerAlias www.m7qd5n2qcdxnsduwglff3k2moctrvodtqgzag3n6isrp7xj7v3nhzmad.onion ... </VirtualHost>
Just as the ‘Location
‘ header redirects a web browser to a new URI, you can also seamlessly redirect any visitors of your site to your new ‘.onion
‘ domain using the ‘Onion-Location
‘ header. To do so, update your existing clearnet vhost file to include the ‘Header set Onion-Location
‘ line below:
<VirtualHost *:443> ... Header set Onion-Location "http://www.m7qd5n2qcdxnsduwglff3k2moctrvodtqgzag3n6isrp7xj7v3nhzmad.onion%{REQUEST_URI}s" ...
And reload apache
apache2ctl -t && systemctl reload apache2
WordPress add_filter() domain rewrites
Now, what do we do about the content of your wordpress site? How do we make sure that a user browsing your wordpress site using its .onion
domain won’t be presented with an anchor link back to the clearnet site? Or that they won’t be given resources (eg js files) loaded from the clearnet site? That would defeat much of the security benefits of them using your .onion
domain!
One solution (modified from Roger Comply’s original post from 2017) is to add a bunch of hooks into your wordpress and theme functions that will regex find & replace your clearnet domains with your .onion
domains.
Because this code will need to be requrie()
d into your child theme’s functions.php
files, we put it in a file called ‘common_functions.php
‘ directly in the ‘wp-content/themes/
‘ directory.
cd /var/www/html/wordpress/htdocs/wp-content/themes cat > common_functions.php <<'EOF' <?php ################################################################################ # Author: Michael Altfield <michael@michaelaltfield.net> # Created: 2021-01-04 # Updated: 2021-01-04 # Version: 0.1 # Purpose: find & replace clearnet domains with .onion. For more info, see: # * https://tech.michaelaltfield.net/2021/02/12/wordpress-multisite-tor/ ################################################################################ ############### # ONION STUFF # ############### # inspired by the following blog post # * https://blog.paranoidpenguin.net/2017/09/how-to-configure-wordpress-as-a-tor-hidden-service/ if( defined('ONION_DOMAIN') ){ # $all_options = wp_load_alloptions(); # $my_options = array(); # foreach ( $all_options as $name => $value ) { # $my_options[ $name ] = $value; # } # error_log( print_r($my_options, true) ); add_filter('option_home', 'onion_rewrite', PHP_INT_MAX); add_filter('option_siteurl', 'onion_rewrite'); add_filter('option_widget_text', 'onion_rewrite'); add_filter('option_widget_media_video', 'onion_rewrite'); add_filter('option_theme_mods_orfeo', 'onion_rewrite'); add_filter('option_wp_seo', 'onion_rewrite'); add_filter('option_wpseo_social', 'onion_rewrite'); add_filter('option_theme_mods_hestia', 'onion_rewrite'); add_filter('bloginfo', 'onion_rewrite'); add_filter('post_link', 'onion_rewrite'); add_filter('page_link', 'onion_rewrite'); add_filter('post_type_link', 'onion_rewrite'); add_filter('category_link', 'onion_rewrite'); add_filter('tag_link', 'onion_rewrite'); add_filter('author_link', 'onion_rewrite'); add_filter('day_link', 'onion_rewrite'); add_filter('month_link', 'onion_rewrite'); add_filter('year_link', 'onion_rewrite'); add_filter('nav_menu_link_attributes', 'onion_rewrite'); add_filter('home_url', 'onion_rewrite'); add_filter('includes_url', 'onion_rewrite'); add_filter('plugins_url', 'onion_rewrite'); add_filter('content_url', 'onion_rewrite'); add_filter('admin_url', 'onion_rewrite'); add_filter('feed_link', 'onion_rewrite'); add_filter('stylesheet_uri', 'onion_rewrite'); add_filter('attachment_link', 'onion_rewrite'); add_filter('wp_get_attachment_image_src', 'onion_rewrite'); add_filter('wp_get_attachment_link', 'onion_rewrite'); add_filter('the_excerpt', 'onion_rewrite'); add_filter('the_content', 'onion_rewrite'); # don't redirect for the .onion. Just don't. add_filter('redirect_canonical', 'custom_redirect_canonical', PHP_INT_MAX ); } function custom_redirect_canonical($content) { return false; } function onion_rewrite($content) { # descend recursively into arrays with content inside them # (necessary for option_widget_text, for example) if( is_array($content) ){ foreach( $content as $key => $value ){ $result[$key] = onion_rewrite($value); } $content = $result; } else { $onion_domains[] = ["/https?:\/\/(www\.)?example1.com/", "m7qd5n2qcdxnsduwglff3k2moctrvodtqgzag3n6isrp7xj7v3nhzmad.onion"]; #$onion_domains[] = ["/https?:\/\/(www\.)?example2.com/", "TODO.onion"]; foreach( $onion_domains as $od ){ $clearnet_regex = $od[0]; $onion_domain = $od[1]; $content = preg_replace( $clearnet_regex, "http://" .$onion_domain, $content); } } return $content; } ?> EOF
A few notes about the above file:
- Obviously, you should replace ‘
example1.com
‘ with your actual clearnet domain and ‘m7qd5n2qcdxnsduwglff3k2moctrvodtqgzag3n6isrp7xj7v3nhzmad.onion
‘ with your actual.onion
domain - These filters are set only if the
ONION_DOMAIN
constant is set. This is done by thewp-config.php
file (see above section) when the$_SERVER['SERVER_NAME']
matches a.onion
regex. - There’s a commented-out block that can be useful for debugging. Uncomment it and check your error log if you find your site still has some references to the old clearnet domain when browsing your site on the
.onion
domain. You may need to add filters for additional options depending on the themes and plugins you’re using. - Most of these filters just pass various options and data fields from the DB through a
onion_rewrite()
function that does the regex replace - The
onion_rewrite()
function can handle both scalars and arrays. It has some logic to recursively call itself for every item in an array. This is necessary for some options, such asoption_widget_text
, for example. - The
onion_rewrite()
function defines an array of$onion_domains
and loops through them. This allows you to define multiple clearnet or.onion
domain or subdomain mappings - There is also a second function that overrides the
redirect_canonical()
function and makes it alwaysreturn false
. This was necessary to stop some stubbornhttp -> https
redirects.
Now, add the following lines to your child theme’s functions.php
file to enable this .onion
find & replace logic for the corresponding site.
# common stuff for all themes: # * .onion domains (see https://tech.michaelaltfield.net/2021/02/12/wordpress-multisite-tor/) require_once( realpath(dirname(__FILE__)) . '/../common_functions.php' );
Add Mercator Alias
To tell wordpress which site to serve for a given .onion
address, we must add it as an alias in Mercator.
Login to your main wordpress site and navigate to My Sites -> Network Admin -> Sites.
Find the wordpress site to which you wish to point your new .onion
domainf and click the “Edit” button under it.
Click the “Add New” button to add a new alias for this wordpress site.
In the “Domain Name” field, enter your new .onion
address, tick the “Mark alias as active” box, and click the “Add Alias” button.
Setup Complete
That’s it! You should now be able to visit your .onion address in the Tor Browser.
Limitations/Improvements
Not for Hidden Services
This guide assumes that you’re running a Onion Service that’s not hidden — since your existing websites are already accessible on the clearnet, we’re only adding a .onion domain for the security benefits of our users.
Proposal 260
Fun fact: the most popular website on the darknet is facebook. There are hundreds of other popular sites on the darknet, including debian, the CIA, the NYT, the BBC, ProPublica, and michaelaltfield.net.
All of these organizations chose to make their websites available over .onion
addresses so that their clients can benefit from the numerous security benefits of the Tor network — but none of them require their onion service to be a “Hidden Service”.
The way that an Onion Service works (by default) is to setup a double-circuit (3-hops client-side + 3-hops server-side = 6 hops) to anonymize both the client and the server. But if you actually don’t need the server to be hidden, then you can optimize the connection (read: reduce client latency) by cutting-out one of the tor circuits on the server-side (so 3-hops only on the client-side).
I intentionally did not include this optimization in my config, because there are some risks of making your clients statistically distinguishable from other Tor clients — which is something I want to avoid.
However, if optimizing latency is more important to you than the anonymity of your clients, you may want to look into the HiddenServiceNonAnonymousMode
and HiddenServiceSingleHopMode
options.
Alternatives
This articles is not the first to attempt to document and assist folks trying to make their clearnet websites accessible on a .onion
domain for their visitors.
This section will document similar articles and projects that the reader may be intersted in investigating.
Enterprise Onion Toolkit
The Enterprise Onion Toolkit (EOTK) was created in early 2017 by Alec Muffett as a tool to help bring existing, complex websites on the clearnet onto the darknet with tor onion services.
EOTK is a tool to easily create a .onion
proxy to any existing clearnet website. Given a set of clearnet domain names, it compiles tor & nginx, and it spits-out a config that will automagically proxy the clearnet sites and dynamically rewrite the clearnet domains to the .onion
domains.
The biggest con of using EOTK is that it can’t automatically fix https issues. For that, you’d need to make your CMS aware of the .onion
domain and https->http switching, as we’ve demonstrated in this article.
Until Let’s Encrypt supports issuing certificates for .onion
domains, EOTK is a great choice only if your organization wants to spend hundreds of dollars every year on a .onion
EV certificate from DigiCert.
Onion Spray
OnionSpray is a fork of EOTK.
Further Reading
- Monitoring .onion sites
- Best Practices for Hosting Onion Services by Riseup
- Feature request for Let’s Encrypt to support issuing https certs for .onion domains
- Facebook announces their Tor Onion Service Oct 2014
- ProPublica announces their Tor Onion Service Jan 2016
- The New York Times announces their Tor Onion Service Oct 2017
- BBC News announces their Tor Onion Service Oct 2019
- michaelaltfield.net announces their Tor Onion Service Jan 2021
Struggling to follow this guide?
If your organization has a wordpress site that you’d like to bring on the darknet, but you can’t get wordpress to play nice, please contact me about your project.
I can provide support for an hourly fee.
Related Posts
Hi, I’m Michael Altfield. I write articles about opsec, privacy, and devops ➡
hello
thankyou for your article but I have a qestion !
How can I make a wordpress site Just on an Darknet (.onion) server
That’s outside the scope of this article, and I don’t recommend it. WordPress is a web application that’s designed to initiate queries to the Internet. It’s not a good fit for an Onion Service that needs to remain anonymous.