How to configure Hugo as a Tor hidden service

· ParanoidPenguin.net @proshe.sh

How to mirror your goHugo based clearnet site as a Tor hidden service with a simple bash script for automated build and deployment.

After migrating my blog from WordPress to Hugo, I wanted to find a simple solution that allowed me to mirror my blog content effortlessly to my hidden services. As Hugo is a static content generator, I didn't have the opportunity to dynamically rewrite content on the fly by pulling the HTTP host from the current request.

The interesting part of the challenge was to find a solution that would let me point three virtual sites to the same web root, thus replicating my original WordPress configuration. Likewise, I only want to maintain a single local Hugo installation.

Prerequisites #

Tor should already be installed and running with hidden services configured. Otherwise, visit the Tor Project and read up on their documentation.

Replicating local and remote directory structure #

I'm using Nginx server blocks (virtual hosts) to serve my content. The root directory for my blog was previously /var/www/blog.paranoidpenguin.net/html/. To allow serving content for additional websites (onion services) under the same (web) root, I'll add an additional level of subdirectories below the html folder.

I'll use an identical directory structure from within my local Hugo installation when building each website using Hugo's --destination flag. That should make it easy to remember what goes where. That leaves me with the following local and remote directory structure:

Hugo multidomain setup

Hugo flags and environment variables #

In theory, Hugo provides us with the functionality we need to mirror content across multiple domains.

Flags:

Environment variables:

However, as my website has been exported from WordPress, it does contain a multitude of absolute URL references as part of the content itself. That, unfortunately, won't be solved by using the --baseURL flag during build time.

To remedy this issue, I'll have to use the sed command to perform a recursive "search and replace" on selected file extensions. More on that later.

Nginx server blocks #

Let's start with a bare-bones virtual host configuration for slackiuxopmaoigo.onion. The configuration includes a location block for the /wp-content folder. Wp-content is my Hugo static folder, and I don't want to duplicate the same static content (multimedia) across all three websites. Therefore, all requests for content residing in /wp-content will be served from my blog folder.

server {

    listen 80;
    server_name slackiuxopmaoigo.onion;

    root /var/www/blog.paranoidpenguin.net/html/onionv2;
    index index.html;

    location /wp-content {
        root /var/www/blog.paranoidpenguin.net/html/blog;
    }

    location / {
        try_files $uri $uri/ =404;
    }

}

Make sure that the listen directive matches what you specified in your tor configuration. For reference:

# HiddenServicePort x y:z
# Redirect requests on port x to the address y:z.

HiddenServiceDir /var/lib/tor/hidden_service/
HiddenServicePort 80 127.0.0.1:80

Rinse and repeat for additional domains and restart Nginx.

The build and deploy script #

I've made a simple bash script to build my clearnet and hidden services. It takes care of running the required Hugo commands and modifies the generated content if needed. Additionally, the script publishes the finalized websites to a remote server using rsync over SSH.

  1#!/bin/sh
  2
  3# A Hugo build and deploy script supporting onion domains.
  4# License: https://blog.paranoidpenguin.net/license/
  5
  6GOHUGO=$(which hugo)
  7
  8SSH_SERVER=server4.paranoidpenguin.net
  9SSH_USER=root
 10
 11REMOTE_DIR=/var/www/blog.paranoidpenguin.net/html
 12REMOTE_USER=user
 13REMOTE_GROUP=group
 14
 15CONTENT_DIR=${HOME}/workspace/hugo
 16STATIC_DIR=wp-content
 17BLOG_URI=https://blog.paranoidpenguin.net
 18BLOG_DIR=blog
 19
 20ONION_V2_TITLE=Slackiuxopmaoigo.onion
 21ONION_V2_URI=http://slackiuxopmaoigo.onion
 22ONION_V2_DIR=onionv2
 23
 24ONION_V3_TITLE=Slackiuxopmaoigo.onion
 25ONION_V3_URI=http://4hpfzoj3tgyp2w7sbe3gnmphqiqpxwwyijyvotamrvojl7pkra7z7byd.onion
 26ONION_V3_DIR=onionv3
 27
 28function prepare ()
 29{
 30    PUBLISH_DIR=$1
 31
 32    if [ -d "${CONTENT_DIR}" ]
 33    then
 34        if [ -d "${CONTENT_DIR}/${PUBLISH_DIR}" ]
 35        then
 36            echo "Recursively deleting ${CONTENT_DIR}/${PUBLISH_DIR}"
 37            rm -Rf ${CONTENT_DIR}/${PUBLISH_DIR}
 38        fi
 39    else
 40        exit 1
 41    fi
 42}
 43
 44function clearnet_builder ()
 45{
 46    PUBLISH_DIR=$1
 47    prepare $PUBLISH_DIR
 48
 49    echo "Building ${CONTENT_DIR}/${PUBLISH_DIR}"
 50
 51    cd $CONTENT_DIR && \
 52    $GOHUGO --destination=${CONTENT_DIR}/${PUBLISH_DIR}
 53
 54    publish $PUBLISH_DIR
 55}
 56
 57function onion_builder ()
 58{
 59    PUBLISH_DIR=$1
 60    ONION_URI=$2
 61    TITLE=$3
 62
 63    prepare $PUBLISH_DIR
 64
 65    BLOG_URI_ESCAPED=(${BLOG_URI//./\\.})
 66    BLOG_URI_ESCAPED=(${BLOG_URI_ESCAPED//\//\\/})
 67
 68    ONION_URI_ESCAPED=(${ONION_URI//./\\.})
 69    ONION_URI_ESCAPED=(${ONION_URI_ESCAPED//\//\\/})
 70
 71    echo "Building ${CONTENT_DIR}/${PUBLISH_DIR}"
 72
 73    cd $CONTENT_DIR && \
 74    HUGO_TITLE=${TITLE} \
 75    $GOHUGO \
 76    --destination=${CONTENT_DIR}/${PUBLISH_DIR} \
 77    --baseURL=${ONION_URI}/ \
 78    --disableKinds=sitemap,RSS
 79
 80    echo "Recursively deleting static content from ${CONTENT_DIR}/${PUBLISH_DIR}/${STATIC_DIR}"
 81    rm -Rf ${CONTENT_DIR}/${PUBLISH_DIR}/${STATIC_DIR}
 82    find ${CONTENT_DIR}/${PUBLISH_DIR} \( -name "*.html" -o -name "*.xml" -o -name "*.css" \) -exec sed -i "s/${BLOG_URI_ESCAPED}/${ONION_URI_ESCAPED}/gI" {} \;
 83
 84    publish $PUBLISH_DIR
 85}
 86
 87function publish ()
 88{
 89    PUBLISH_DIR=$1
 90
 91    echo "Syncing ${CONTENT_DIR}/${PUBLISH_DIR}/ ---> ${REMOTE_DIR}/${PUBLISH_DIR}"
 92    rsync -azzq --chown=${REMOTE_USER}:${REMOTE_GROUP} ${CONTENT_DIR}/${PUBLISH_DIR}/ ${SSH_USER}@${SSH_SERVER}:${REMOTE_DIR}/${PUBLISH_DIR}
 93
 94    echo "Rsync has finished."
 95    sleep 3
 96    clear
 97}
 98
 99clear
100clearnet_builder $BLOG_DIR
101onion_builder $ONION_V2_DIR $ONION_V2_URI $ONION_V2_TITLE
102onion_builder $ONION_V3_DIR $ONION_V3_URI $ONION_V3_TITLE

Obviously, a local and remote backup should be in place before running a script you downloaded from a stranger on the Internet ;-)

A quick breakdown of the script #

  1. Prepare

    Delete the specified publish directory before building a new website.

  2. Clearnet builder

    Builds my clearnet website using the --destination flag.

  3. Onion builder

    This function will build the onion sites and perform a search and replace using sed to fix absolute URLs referencing my clearnet site.

    In addition to the --destination flag, I'm also using the --baseURL flag to provide the correct hostname (and path) to the root. The --disableKinds flag removes content I don't want to offer from my onion sites.

    After building the site, and before running the sed command, I'm escaping dots and slashes from absolute URLs to make it work with sed. I was unable to make that happen with a oneliner in bash, but I guess it will do.

  4. Publish

    Establishes an SSH connection (using SSH keys) with the remote server. Rsync is used to publish content to the specified remote folder while passing the chown command for the required user and group.

Addendum #

The build scripts are available on Github from this repository.

Built on Arch Linux with:


All content stolen from ParanoidPenguin.net as permitted under the WTFPL license.