Running Ghost on Tor
Recently I've had the opportunity to play with some new and existing technologies as a mechanism of both upskilling and trying something new.
I decided to spend some of that time learning how to create a hidden service, and make my own blog available over the Tor network. Although this task was mainly as a proof of concept so I could say I've done it, I did have a tiny desire to be lucky and brute force an awesome vanity Tor .onion address.
Creating my address
Remembering that I had some free Google Cloud Platform (GCP) credits, I spun up some servers and cloned mkp224o, the vanity address generator for ed25519 onion services, onto each of them. I opted for CPU optimised instances as this was my limitation using the tool.
Knowing that a 10-character prefix (adammalone) was potentially out of my computational reach, I decided to temporarily calculate an easier hash with a shorter known prefix. This would allow me to move on to the next step in my PoC while the GCP servers continue churning away for as long as I still have free credits.
I ended up calculating a hash with the prefix amalone in an unbelievably lucky 30 seconds.
This blog post provides some further recommended reading for people more interested in how .onion
hostnames can be generated.
Installing tor
Continuing the trend I've written about in my previous blog posts, I had to find a way to install and configure everything using Ansible. Ultimately I ended up using haghighi_ahmad.tor with some pretty minor configuration in vars/main.xml
This configures tor to listen on port 80 and to forward requests through to port 88 where Nginx is listening for it. Whilst there is an existing Nginx server block using port 80, I wanted to segregate Tor further.
N.B. Tor can listen on port 80 at the same time as Nginx. The reason for this is that Tor isn't binding to an external interface as Nginx does.
The above configuration provides me with an /etc/tor/torrc
that looks like the below:
Configuring Ghost to listen to an onion
One of the limitations of Ghost is its inability to respond to multiple different domains. The domain that each Ghost blog uses to serve pages is hardcoded in config.production.yml
and any attempt to access the site with a different URL leads to either redirects or errors.
As Tor uses a different way entirely of representing a hostname, this blog would need to be accessible using two entirely separate combinations of words in the browser's URL bar. I tried initially to use some clever Nginx configuration to point requests coming in on the .onion
domain to the clearnet domain by rewriting on the way in and out.
That unfortunately proved fruitless since I assume Ghost uses the URL configured in config.production.yml
to create links and routes rather than the Host
header associated with incoming requests. This approach is good from a security perspective, but the limit of a single domain makes this sort of implementation challenging.
I eventually settled on creating a shadow install of Ghost for the .onion
domain that would mirror the clearnet domain. I achieved this by creating a new directory for the Tor install and symlinking content
, current
, system
, and versions
directories to the clearnet install. I copied across config.production.yml
and changed only the url
and server: port
values.
N.B. MySQL details should remain the same as we're reading from the same clearnet database regardless of which install the user accesses.
I could potentially use the .onion
hostname as my production URL and then construct another Cloudflare Worker to rewrite links and alter request/response Host
headers for anyone coming in on the clearnet domain, but that seemed like too much work.
Fitting it all together
After learning about the fantastic drawing tool Excalidraw, I felt it only appropriate to draw a pretty picture to show how users may reach the server over either HTTPS or Tor.
What can be seen in the diagram below is that users browsing over standard HTTP/S will be converted to HTTPS with Cloudflare before going through Nginx to my Ghost public instance.
Users browsing with Tor pop out inside the server, bypassing the firewall and hit Nginx on port 88. These requests are then routed to the tor instance of Ghost due to the limitation discussed above.
As discussed above, both instances of Ghost hit the same MySQL database. The below configuration shows how requests to the administration pages are blocked which I think makes the concept of two ghosts one db more of a safe one.
N.B. Security headers set in Nginx have been removed from these snippets for brevity.
Why no SSL certificate?
Finally, you may have noticed that I've not utilised an SSL certificate for users accessing the site over Tor. After a good deal of research, my opinion has coagulated to the view that over Tor, SSL certificates provide positive identity but no additional security.
To summarise this very good Stack Overflow comment:
- As Tor is already an encrypted protocol, an SSL certificate adds no additional security
- Anyone can generate an
.onion
hostname although it's cryptographically all but impossible for someone to generate your.onion
hostname - An SSL certificate with EV extension can prove the real identity of the owner of the authenticated hosts
Because I'm not looking to positively identify myself as the owner of this blog any further than I already have, I'm happy to not go through the additional effort, time, and cost of using an unnecessary SSL certificate.
Find me
Until I strike cryptographic gold with a nice 10-character prefix, you can find me on Tor here: http://amalone2l6sqxt75shmkrbglepe5uawm4gr5gjk4w7h4l3qsao7iwcqd.onion.