5 min read

Running Ghost on Tor

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

tor_proxy: false
tor_http_port: "80"
tor_nickname: "torphonius"
tor_run_as_daemon: true
tor_user: "debian-tor"
tor_group: "debian-tor"
tor_hidden_service_dir: "/var/lib/tor/hidden_service"
  - name: http
    version: 3
    port: 80
    host: ""
Configuration variables for Ansible Tor role.

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:

RunAsDaemon 1

# Hidden services
HiddenServiceDir /var/lib/tor/hidden_service/http
HiddenServiceVersion 3
HiddenServicePort 80
My server's /etc/tor/torrc.

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.

Network diagram to show mechanisms users may interact with this blog.

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.

server {
    listen 443 ssl http2;
    server_name www.adammalone.net;
    index index.html index.htm;

    ssl_certificate     /etc/ansible/keys/www.adammalone.net-ec.pem;
    ssl_certificate_key /etc/ansible/keys/www.adammalone.net-ec.key;

    include /etc/nginx/cloudflare-allow.conf;
    deny all;
    location / {
            proxy_pass http://localhost:2112/;
Nginx configuration produced by Ansible for the clearnet blog.

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.

server {
    root /var/www/html/tor;
    index index.html index.htm;

    location /ghost { deny all; }
    location / {
            proxy_pass http://localhost:2113/;
Nginx configuration produced by Ansible for the onion blog.
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.