Michael Altfield's gravatar

WordPress Multisite on the Darknet (Mercator .onion alias)

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.

How to use a .onion with wordpress multisite

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:

  1. Debian 10
  2. Apache 2.4
  3. Tor 0.3.5.10
  4. WordPress Multisite 5.4.1
  5. 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:

  1. Install & configure system-level packages (eg tor) and
  2. 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.

⚠ BEWARE: If you are trying to make your wordpress site available only on the darknet to preserve the anonymity of your server and hosting provider, then this guide is not for you!

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 that include()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 in wp-config.php). Please remove or comment out that define() 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';
}
Logo for Let's Encrypt

Let's Encrypt currently does not issue certs for .onion domains, but there's an open feature request for it

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:

  1. ServerName
  2. ServerAlias, if applicable
  3. Listen
  4. 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:

  1. Obviously, you should replace 'example1.com' with your actual clearnet domain and 'm7qd5n2qcdxnsduwglff3k2moctrvodtqgzag3n6isrp7xj7v3nhzmad.onion' with your actual .onion domain
  2. These filters are set only if the ONION_DOMAIN constant is set. This is done by the wp-config.php file (see above section) when the $_SERVER['SERVER_NAME'] matches a .onion regex.
  3. 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.
  4. Most of these filters just pass various options and data fields from the DB through a onion_rewrite() function that does the regex replace
  5. 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 as option_widget_text, for example.
  6. 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
  7. There is also a second function that overrides the redirect_canonical() function and makes it always return false. This was necessary to stop some stubborn http -> 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.

Screenshot of the wordpress multisite dashboard

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.

Screenshot of the wordpress multisite dashboard

Click "Edit" under the site for which you want to create a .onion alias

Click the "Add New" button to add a new alias for this wordpress site.

Screenshot of the wordpress multisite dashboard

Click "Add New"

In the "Domain Name" field, enter your new .onion address, tick the "Mark alias as active" box, and click the "Add Alias" button.

Screenshot of the wordpress multisite dashboard

Enter the .onion address into "Domain Name" field, tick the "Mark alias as active" box, and click "Add Alias"

Screenshot of the wordpress multisite dashboard

Alias Created.


Setup Complete

That's it! You should now be able to visit your .onion address in the Tor Browser.

Screenshot of michaelaltfield.net on the clearnet

michaelaltfield.net on the clearnet

Screenshot of michaelaltfield.net when visited in the Tor Browser via the .onion address

michaelaltfield.net's .onion viewed in the Tor Brwoser

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.

Further Reading

  1. Monitoring .onion sites
  2. Best Practices for Hosting Onion Services by Riseup
  3. Feature request for Let's Encrypt to support issuing https certs for .onion domains
  4. Facebook announces their Tor Onion Service Oct 2014
  5. ProPublica announces their Tor Onion Service Jan 2016
  6. The New York Times announces their Tor Onion Service Oct 2017
  7. BBC News announces their Tor Onion Service Oct 2019
  8. 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

Leave a Reply

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>