Build your own shrine

How to build a static website as a Tor hidden service, while staying totally private.

Introduction

Privacy is one component of a healthy society. In public, we are confined to a society built on activities that no one hates – a gray slurry of what a panel of lawyers and insurance adjusters couldn’t find fault with. But in private we have a wonderful freedom, to be who we are moment to moment, tailoring our conversation and expression only to the people we’ve invited to be there with us. It is a great enabler of speech that would never be spoken without it, of things that would never get done in public.

Privacy is freedom. Privacy is a good thing for its own sake.

I think that the default society of the 21st century in the United States is less private than it ought to be, and that this impoverishes us individually and our society as a whole.

In the abstract, this document is several things:

But in the specific, it has one goal: to show how someone with something to say, a bit of money, and a willingness to work, can share something with the world anonymously.

It will explain how to obtain money, buy hosting, and publish words on the Internet, without tying those words to a public identity.

Ingredients

Before you begin, make sure you have access to the following:

We keep prerequisites to a minimum, which makes this easier to explain, and keeps us more secure as we rely on fewer third parties.

This process involves using Tor (of course) and the command-line.

If you already have a password manager, you may wish to set up a separate one dedicated to your Tor identity.

Building and maintaining it will also involve a bit of money. Money is required to get hosting that isn’t associated with your identity, and a bit of extra money will speed up a required step of getting an email address.

Technical goals

Tor

The most secure option is to use Tails or Qubes.

You can connect to Tor on your regular computer using the Tor Browser, or via a specialized operating system like Tails or Qubes. The former is easier, but the latter is more secure.

When building this shrine, I chose to do it all on my regular desktop. If I were engaging in activity that was illegal in my jurisdiction, however, I’d have used Tails or Qubes instead.

Aside: Recommendation from DNM Bible

The DNM Bible is a guide geared towards buying illegal products (mostly drugs) on darknet markets. Their strong recommendation is to use Tails directly for these transactions, and to avoid macOS and Windows, because

  1. “Windows, and Mac are loaded with backdoors, and both are companies that WILL cooperate with law enforcement.”
  2. “This guide is written to help keep you safe, and secure. Always be prepared for the worst case scenarios, people using Windows/Mac are low hanging fruit, you are a lot more likely to fall into the lap of LE.”
  3. “Also keep in mind one of your number one rules should always to keep your Darknet life, and your real life seperate. Would you want someone from the Darknet getting their hands on your normal files? Fuck no you don’t so don’t even let that be an option.”

In my opinion, the third reason is the most important. The easiest way for a state to link your activity on Tor to your offline identity is by making a mistake. Opsec is very difficult to maintain, especially if your adversary is a global power. For this reason alone, you should consider using Tails or Qubes.

The first reason is a combination of false and misleading. Windows and macOS are not “loaded with backdoors”, and the claim is paranoid bordering on delusional. It’s true that Microsoft and Apple will cooperate with law enforcement, but this is true of virtually everyone, and they can only give up information they actually have. The question “what information do these companies have about me” is a good one, and it’s hard to answer, which is why claims like “Windows, and Mac [sic] are loaded with backdoors” feel tempting to make, but it’s better to focus on the true uncertainty of what information a company has about you than the false certainty that they are operated by the same lizard people that control the government and put Lithium in the water supply.

The second reason, about low hanging fruit, is true, but it’s a tradeoff. For the desktop use case in particular, Windows and macOS receive more attention of researchers looking to both attack and defend their users than Linux OSes do. This means that there are more advanced attacks being developed against web browsers on popular operating systems, and more advanced defenses being developed internally at Microsoft and Apple. It’s not clear which of these is actually more likely to keep you private on Tor, although I think you could make a good argument for either case.

More secure: Using a specialized operating system

You can connect to Tor using a live OS like Tails. Under this method, you write the Tails OS to a USB stick, shut down your computer, and boot from the stick. You can keep important files on a persistent partition. In Tails, all Internet connections go through Tor, so you can be sure that you haven’t accidentally clicked a link that could de-anonymize you in the wrong browser.

You could also use Qubes, which is a Linux OS that is designed to run applications in virtual machines for security. You can create a proxy VM that runs Tor, and a second VM for building the site that only allows connections to the Tor VM. This is more complex to set up and requires deeper Linux system administration knowledge, but it is also more flexible, as you can browse the web on Tor and on the clearnet side by side, unlike with Tails.

Both of these options can provide greater certainty than using your main computer to use Tor, at the cost of some convenience. The biggest advantage is that they make it hard to accidentally click on the wrong thing in the wrong context, which might de-anonymize you.

Conveniences of the less convenient method

We noted earlier that using your regular computer to connect to Tor was more convenient, and that is true in general, but it’s worth noting one nicety of using a specialized OS for connecting to Tor: running commands like git, ssh, and rsync through Tor is easier. We don’t need to use SOCKS proxies or special config files as described in the next section; we can just run those commands the way we always do and data will be sent over Tor transparently.

Easier: Connecting to Tor on your regular desktop

We can browse the web privately through Tor with Tor Browser, but using command-line tools requires configuring them to use the built-in Tor proxy.

This only works while the Tor Browser is running.

The Tor Browser starts a SOCKS5 proxy server on port 9050.

(Note that this port number may change; see Troubleshooting for how to deal with this.)

Running SSH over Tor

We can use checkmyip to see our public IP address over SSH, and use that to prove that SSH is working over Tor before connecting to our new VPS.

First, run a command like this to check your current public IP.

ssh root@sshmyip.com

That should return something like this (real values redacted):

Result from sshmyip
{
"comment": "##     Your IP Address is 1.2.3.4 (56789)     ##",
"family": "ipv4",
"ip": "1234",
"port": "56789",
"protocol": "ssh",
"version": "v1.3.0",
"force_ipv4": "ipv4.telnetmyip.com",
"force_ipv6": "ipv6.telnetmyip.com",
"website": "https://github.com/packetsar/checkmyip",
"sponsor": "Sponsored by ConvergeOne, https://www.convergeone.com/"
}

Now we can run SSH over Tor and compare the result:

ssh -o ProxyCommand='nc -x 127.0.0.1:9050 %h %p' -lroot sshmyip.com

That should return the IP address of your Tor exit node, different from your actual public IP address.

Note that this works just as well with onion addresses, but we don’t have one to connect to yet.

Using curl over Tor

Curl has native support for socks5 proxies. We use the --socks5-hostname argument to send both the actual web requests and also the DNS requests of the web servers over Tor. (The similar --socks5 option sends the web requests over Tor, but leaks DNS requests to the system DNS resolver, and also cannot work with .onion names.)

You can use curl over Tor with commands like:

# Retrieve the public IP address of the Tor exit node
curl --socks5-hostname localhost:9050 curl --socks5-hostname localhost:9050 https://canhazip.com

# Retrieve the Tor homepage from its .onion domain
curl --socks5-hostname localhost:9050 http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion/
Using Git over Tor: Downloading this site’s code

I deployed a Git server so that I can publish the source code for the shrine and the source code for the shrine’s theme. These are currently available only on Tor.

To clone them, we tell Git to use a SOCKS proxy. Similar to curl’s --socks5-hostname, the socks5h:// scheme sends DNS requests over the proxy, while the socks5:// (without the trailing h) scheme would leak DNS requests and not work with .onion domains.

# This is the Onion service name for my Git server.
# You can also navigate to it in Tor Browser to examine the code that way.
sacredgit=xdhbqaloeg5bid3tzjpllhnxisdguz34xtoc7qegnmh3essuesfdyqid.onion

# Clone the site
git clone "http://$sacredgit/shrineadmin/sacredground" --config "http.proxy=socks5h://localhost:9050"

# Clone the theme
git clone "http://$sacredgit/shrineadmin/hugo-theme-onionskin" --config "http.proxy=socks5h://localhost:9050"

# Note that the `--config ...` line only needs to be specified on the initial clone,
# as it saves that configuration option in the local clone.
cd ./sacredground
git config --get http.proxy
# returns 'socks5h://localhost:9951'
If you plan to commit to any Git repositories behind Onion services, take care that your identity doesn’t leak unintentionally. You can use --config user.name="Your Pseudonym" --config user.email="pseudonym@example.com" to clone a repo with a specific name/email.
Troubleshooting

Tor Browser will configure a local proxy server that we can use to run other tools over Tor. This is supposed to have a value of 9050 by default, but I found that nothing could connect with that value. I opened about:config in Tor Browser, and found that extensions.torbutton.custom.socks_port was set to a different value (in my case 9170). Using that value instead of the default worked.

Money

To obtain an email address and hosting services, you will need some money.

What I recommend here is sold in Euros. To get an email address right away is €13/year, but if you’re willing to wait for 48 hours you can get it for free. The hosting service is €15/mo for a small VPS.

I recommend Monero (XMR) for this, as it is well known to preserve your privacy. It may be a bit difficult to obtain, however because it is so privacy-preserving you can safely buy some from a centralized exchange with an account tied to your identity, which is not the case for Bitcoin. Just like taking cash out of the bank, the exchange knows how much Monero you buy, but doesn’t know where you spend it later.

Selecting a Monero wallet

To use Monero you will need a wallet. I used Feather (clearnet, onionlink) which is a locally-synchronizing wallet that connects to remote nodes to obtain the blockchain state. When using Feather, your private key stays only on your own device, and you don’t need to download the entire blockchain yourself.

I have also used the first-party Monero client before in the past. Its biggest downside is that it takes a lot of disk space.

You can get the official Monero wallet as well as links to other wallets from Monero (clearnet, onionlink) .

Setting up Feather

The set up process is easy to follow; just download and launch the program, and it will guide you through creating a new wallet.

Make sure to save the wallet seed phrase to your password manager when it prompts you.

How to buy Monero in general

See the Merchants page on the Monero website. (It’s worth noting that Local Monero has an Onion service as well as the clearnet page mentioned there: http://nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion/nojs/). See also How to buy Monero on the DNM Bible.

I chose to buy Bitcoin from a centralized exchange, then trade it for Monero.

The easiest way for me to do that actually involves two exchanges – Coinbase and Kraken. Coinbase is happy to connect to my bank account and let me trade cash for cryptocurrency, but it doesn’t offer Monero. Kraken is happy to let me send cryptocurrency to it, and to trade Bitcoin for Monero, but does not connect to my bank to allow for direct purchases. (I’m not sure why this is; maybe a problem on Kraken’s end, or maybe an issue with me being in the US.) By combining them, I could spend US dollars and eventually receive Monero through a somewhat convoluted process.

One thing to note: this may take longer if you don’t already have accounts on the exchange(s) you want to use. Coinbase and Kraken both require proof of government identification and several steps to assure them of your identity, and these steps may take hours or perhaps days.

Exchanges that work this way are required to follow the so-called “know-your-customer” or “KYC” laws that banks must follow, including a strong tie from your account to a government ID that they can give to law enforcement upon request. We use Monero because of its strong privacy guarantees in light of this process – the exchanges can tell that the money you spent with them eventually goes to fund a purchase of some Monero, but they cannot see what happens to the Monero from that point forward.

There is some nuance to this. For instance, if you buy Bitcoin, send the entire amount to a Monero wallet, send the entire Monero amount to someone else, they use the same exchange to trade that Monero back to Bitcoin and/or regular currency, and you repeat this cycle of transactions multiple times, some adversaries may be able to de-anonymize you over time. To counter this, send large blocks of Bitcoin (eg for several months of hosting) to your Monero wallet, and pay smaller amounts month by month. This is called an EABE attack

How I bought Monero

Here’s the strategy that worked for me, in the US.

Accounts: email and hosting

A new email account

We want to create an account on Njalla, but this requires an email or XMPP account.

Unfortunately, most regular providers, including Gmail, require a phone number to create a new account, and if they don’t, they may require a phone number to unlock an account at any time. Getting a phone number that isn’t tied to your main identity is difficult and costs money, so this isn’t ideal.

Proton (clearnet, onionlink) is generally privacy-friendly, but when creating an account it may require an existing email address or phone number, especially when using Tor.

One useful option is Tutanota, which does not require an existing email address or phone number. (Tutanota does not have a .onion hidden service, but it does encourage the use of Tor, and go out of its way to provide service to anonymous users.) However it has a 48 hour waiting period before you can use a free account.

You can wait for this, but you can also circumvent it if you’re willing to pay for the service. And, you can pay via Monero through the third party reseller ProxyStore (clearnet, onionlink) , which offers Tutanota gift codes for €13, and they accept Monero.

Some notes about that transaction:

After you have one email account, obtaining other accounts linked to the first one gets easier. For instance, I could immediately sign up for a Proton account once my Tutanota account could receive email. Proton does not require that the email you use to verify your account remains valid; per their support article on human verification, “We don’t save CAPTCHA results. If you are presented with email or SMS verification, we only save a cryptographic hash of your email or phone number which is not permanently associated with the account that you create.”

(My Proton confirmation did go to spam, so check that if you want to do this.)

Hosting with Njalla

Njalla (clearnet, onionlink) is a VPS hosting company, DNS registrar, and VPN provider. You can pay for their services with Monero, and they do not collect any information about their users except for a point of contact email address. Signing up for an account is instant.

When I created my account, I configured a low-end VPS: €15/mo, 1 core, 1.5GB RAM, 15GB disk, 1.5TB traffic. I chose Alpine Linux 3.16 because I am already a user, but they also offer a more mainstream Debian installation.

When buying this service, you need to select a name for your VPS. I think it is useful to have a naming scheme, even though currently I only ever plan to have this single virtual machine on this project. To keep this separated from a real-life identity, you should use a naming scheme that is generic and could come from anyone. Try something like, African countries, or Canadian provinces, or Greek letter names. I’m going with the NATO Phonetic Alphabet, so my first machine will be called ‘alfa’.

Configuring the VPS will also require giving Njalla an SSH public key. Take care that you generate a new key for this use, so that it cannot be tied to other SSH keys you may have. When generating the key, take care to blank the key comment with -C "", otherwise it will use username@hostname which may be tied to your real world identity. Per the Njalla documentation (clearnet, onionlink) , try something like this:

ssh-keygen -t ed25519 -f ./njalla_ed25519 -C ""

Finally, once you’ve selected your VPS options, set a name, and set the SSH key, it will ask you to fund the transaction. You can send more than the price of the service, and the overage will stay in your account. This is useful because Njalla cannot bill you – if you forget to make a payment, your service will simply be turned off. It may also make it harder to trace transactions if you bought Monero on a KYC exchange, although I’m not certain about this.

I sent 0.5 XMR to fund my account, which was approximately $70 at the time. It took about 30 minutes to be confirmed.

Once the transaction was confirmed, Njalla began provisioning my instance, which took a few more minutes before I could log in.

Buying a domain name with Njalla

Njalla is also a private DNS registrar who will hold domains for you.

This is different, legally, than buying a domain name yourself. From their the Njalla FAQ (clearnet, onionlink) :

We’re not actually a domain name registration service, we’re a customer to these. We sit in between the domain name registration service and you, acting as a privacy shield.

When you purchase a domain name through Njalla, we own it for you. However, the agreement between us grants you full usage rights to the domain. Whenever you want to, you can transfer the ownership to yourself or some other party.

This means that Njalla can rescind your access to the domain in accordance to the terms of their license agreement, which can happen if they are obliged by their own agreements with the registry operators for various TLDs.

The upside is significant, however: this arrangement allows you to control a domain name without giving up any identifying information.

I paid for registration of sacredground.click through Njalla, and pointed it to the VPS. I already had money in my Njalla wallet, and the purchase went through immediately.

> host sacredground.click
sacredground.click has address 80.78.27.69
sacredground.click has IPv6 address 2a0a:3840:8078:27:0:504e:1b45:1337

But Njalla doesn’t know who I am (unless, I suppose, they were to read this website). The whois information is all their contact info, as they are the legal operators of “my” domain.

Whois information for sacredground.click
% IANA WHOIS server
% for more information on IANA, visit http://www.iana.org
% This query returned 1 object

refer:        whois.uniregistry.net

domain:       CLICK

organisation: UNR Corp.
address:      Third Floor, Monaco Towers, Georgetown
address:      Grand Cayman, Cayman Islands
address:      30369SMB-KY11202
address:      Cayman Islands

contact:      administrative
name:         Shayan Rostam
organisation: UNR Corp.
address:      Third Floor, Monaco Towers, Georgetown
address:      Grand Cayman, Cayman Islands
address:      30369SMB-KY11202
address:      Cayman Islands
phone:        +1 345 746 3687 
fax-no:       +1 345 746 3687 
e-mail:       partners@unr.com

contact:      technical
name:         Francisco Obispo
organisation: Tucows Inc
address:      96 Mowat Avenue
address:      Toronto, Ontario M6K3M1
address:      Canada
phone:        +1.949.903.3449
e-mail:       trs-ops@tucows.com

nserver:      NS1.UNIREGISTRY.NET 2620:57:4000:1:0:0:0:1 64.96.1.1
nserver:      NS2.UNIREGISTRY.INFO 64.96.2.1
nserver:      NS3.UNIREGISTRY.NET 185.159.197.3 2620:10a:80aa:0:0:0:0:3
nserver:      NS4.UNIREGISTRY.INFO 185.159.198.3 2620:10a:80ab:0:0:0:0:3
ds-rdata:     65517 13 2 D09A2C14C4382634EDCCF9634FB32E91511E1E642F3C38468BA2407F1D1BAD24

whois:        whois.uniregistry.net

status:       ACTIVE
remarks:      Registration information: http://uniregistry.link

created:      2014-08-15
changed:      2022-09-06
source:       IANA

# whois.uniregistry.net

Domain Name: sacredground.click
Registry Domain ID: DO_b2810c4f45c91ce962d2ffb6958098fd-UR
Registrar WHOIS Server: whois.tucows.com
Registrar URL: www.tucowsdomains.com
Updated Date: 2022-10-23T01:16:24.862Z
Creation Date: 2022-10-23T01:16:23.343Z
Registry Expiry Date: 2023-10-23T01:16:23.343Z
Registrar: Tucows Domains Inc.
Registrar IANA ID: 69
Registrar Abuse Contact Email: domainabuse@tucows.com
Registrar Abuse Contact Phone: +1.4165350123
Domain Status: clientUpdateProhibited https://icann.org/epp#clientUpdateProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: addPeriod https://icann.org/epp#addPeriod
Registry Registrant ID: REDACTED FOR PRIVACY
Registrant Name: REDACTED FOR PRIVACY
Registrant Organization: Data Protected
Registrant Street: REDACTED FOR PRIVACY
Registrant City: REDACTED FOR PRIVACY
Registrant State/Province: Charlestown
Registrant Postal Code: REDACTED FOR PRIVACY
Registrant Country: KN
Registrant Phone: REDACTED FOR PRIVACY
Registrant Fax: REDACTED FOR PRIVACY
Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Registry Admin ID: REDACTED FOR PRIVACY
Admin Name: REDACTED FOR PRIVACY
Admin Organization: REDACTED FOR PRIVACY
Admin Street: REDACTED FOR PRIVACY
Admin City: REDACTED FOR PRIVACY
Admin State/Province: REDACTED FOR PRIVACY
Admin Postal Code: REDACTED FOR PRIVACY
Admin Country: REDACTED FOR PRIVACY
Admin Phone: REDACTED FOR PRIVACY
Admin Fax: REDACTED FOR PRIVACY
Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Registry Tech ID: REDACTED FOR PRIVACY
Tech Name: REDACTED FOR PRIVACY
Tech Organization: REDACTED FOR PRIVACY
Tech Street: REDACTED FOR PRIVACY
Tech City: REDACTED FOR PRIVACY
Tech State/Province: REDACTED FOR PRIVACY
Tech Postal Code: REDACTED FOR PRIVACY
Tech Country: REDACTED FOR PRIVACY
Tech Phone: REDACTED FOR PRIVACY
Tech Fax: REDACTED FOR PRIVACY
Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Registry Billing ID: REDACTED FOR PRIVACY
Billing Name: REDACTED FOR PRIVACY
Billing Organization: REDACTED FOR PRIVACY
Billing Street: REDACTED FOR PRIVACY
Billing City: REDACTED FOR PRIVACY
Billing State/Province: REDACTED FOR PRIVACY
Billing Postal Code: REDACTED FOR PRIVACY
Billing Country: REDACTED FOR PRIVACY
Billing Phone: REDACTED FOR PRIVACY
Billing Fax: REDACTED FOR PRIVACY
Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Name Server: 3-get.njalla.fo
Name Server: 2-can.njalla.in
Name Server: 1-you.njalla.no
DNSSEC: unsigned
URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf/

>>> Last update of WHOIS database: 2022-10-23T01:21:09.990Z <<<

# whois.tucows.com

Domain Name: SACREDGROUND.CLICK
Registry Domain ID: DO_b2810c4f45c91ce962d2ffb6958098fd-UR
Registrar WHOIS Server: whois.tucows.com
Registrar URL: http://tucowsdomains.com
Updated Date: 2022-10-23T01:18:09
Creation Date: 2022-10-23T01:16:23
Registrar Registration Expiration Date: 2023-10-23T01:16:23
Registrar: TUCOWS, INC.
Registrar IANA ID: 69
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: clientUpdateProhibited https://icann.org/epp#clientUpdateProhibited
Registry Registrant ID: 
Registrant Name: REDACTED FOR PRIVACY
Registrant Organization: REDACTED FOR PRIVACY
Registrant Street: REDACTED FOR PRIVACY 
Registrant City: REDACTED FOR PRIVACY
Registrant State/Province: Charlestown
Registrant Postal Code: REDACTED FOR PRIVACY
Registrant Country: KN
Registrant Phone: REDACTED FOR PRIVACY
Registrant Phone Ext: 
Registrant Fax: REDACTED FOR PRIVACY
Registrant Fax Ext: 
Registrant Email: https://tieredaccess.com/contact/8a9b46b4-1c52-475f-abb8-8259d58ae40a
Registry Admin ID: 
Admin Name: REDACTED FOR PRIVACY
Admin Organization: REDACTED FOR PRIVACY
Admin Street: REDACTED FOR PRIVACY 
Admin City: REDACTED FOR PRIVACY
Admin State/Province: REDACTED FOR PRIVACY
Admin Postal Code: REDACTED FOR PRIVACY
Admin Country: REDACTED FOR PRIVACY
Admin Phone: REDACTED FOR PRIVACY
Admin Phone Ext: 
Admin Fax: REDACTED FOR PRIVACY
Admin Fax Ext: 
Admin Email: REDACTED FOR PRIVACY
Registry Tech ID: 
Tech Name: REDACTED FOR PRIVACY
Tech Organization: REDACTED FOR PRIVACY
Tech Street: REDACTED FOR PRIVACY 
Tech City: REDACTED FOR PRIVACY
Tech State/Province: REDACTED FOR PRIVACY
Tech Postal Code: REDACTED FOR PRIVACY
Tech Country: REDACTED FOR PRIVACY
Tech Phone: REDACTED FOR PRIVACY
Tech Phone Ext: 
Tech Fax: REDACTED FOR PRIVACY
Tech Fax Ext: 
Tech Email: REDACTED FOR PRIVACY
Name Server: 1-you.njalla.no
Name Server: 2-can.njalla.in
Name Server: 3-get.njalla.fo
DNSSEC: unsigned
Registrar Abuse Contact Email: domainabuse@tucows.com
Registrar Abuse Contact Phone: +1.4165350123
URL of the ICANN WHOIS Data Problem Reporting System: https://icann.org/wicf
>>> Last update of WHOIS database: 2022-10-23T01:21:10Z <<<

This is different from WHOIS privacy that is offered by other registrars. The registrar for most of my other domains is Gandi. When I buy a domain with Gandi, I usually turn on WHOIS privacy, which lists Gandi’s physical address, email address, and phone number. They act as a proxy and will forward me any communication they receive on my behalf. Unlike Njalla, Gandi has strong links to my real identity, so they could de-anonymize me if they chose to.

Configuring the VPS

Connecting over Tor

Njalla will give you an IPv4 and IPv6 address of your new VPS. You could connect over the clearnet, but we want to obfuscate our connection to this server, so we want to configure Tor to do it.

This section explicitly runs ssh/rsync through a proxy as required when using your main computer for Tor. Remember that specifying proxies is not necessary if using Tails or Qubes.

Even when running in a specialized OS like Tails, it is sometimes useful to save the host in ~/.ssh/config, just make sure to remove the ProxyCommand line.

Since we confirmed earlier that SSH over Tor is working, (and made sure that we had the correct port number if 9050 is not working) we can connect to the VPS with something like:

VPSIP4=1.2.3.4
ssh -o ProxyCommand='nc -x 127.0.0.1:9050 %h %p' root@$VPSIP4

Rather than remember the -o ProxyCommand=... stuff, we can save that in our SSH config file.

### Connect to the VPS's public IP over Tor
Host sacredground.ip
  HostName 1.2.3.4
  User root
  IdentityFile ~/.ssh/njalla_ed25519
  ProxyCommand nc -x 127.0.0.1:9050 %h %p

You can see in $SSH_CLIENT environment variable in your new shell that you’re coming from a Tor exit node. It has three space-separated values: the IP address connected to the server (which will be your Tor exit node), a source port number (random), and a destination port number (22 for SSH by default).

alfa:~# env | grep SSH_CLIENT
SSH_CLIENT=1.2.3.4 56790 22

provision.sh script

Note that the interactive SSH connection feels heinously slow. We will want to avoid interactive SSH as much as possible. To do that we can use a simple script to install packages and change config files for us.

Some notes about this script:

Here is my script:

My provision.sh script
#!/bin/sh
set -eu

#### Variables
#
# Change this to values appropriate for your environment
VPSIP4=80.78.27.69
VPSIP6=2a0a:3840:8078:27::504e:1b45:1337
VPSDNS=sacredground.click
ACMEEMAIL=surveilledmicah@tutanota.com

#### Configuring the base Alpine install
#
if ! grep -q "edge" /etc/apk/repositories; then
cat >>/etc/apk/repositories <<EOF

@edgemain       https://dl-cdn.alpinelinux.org/alpine/edge/main
@edgecommunity  https://dl-cdn.alpinelinux.org/alpine/edge/community
@edgetesting    https://dl-cdn.alpinelinux.org/alpine/edge/testing

EOF
fi

# Ignore apk update errors, which can happen if we run this script several times in a row
apk update || true

apk add apk-autoupdate@edgetesting nginx openssl rsync tor tmux
rc-update add tor default
rc-update add nginx default

# Configure automatic package updates
cat >/etc/apk/autoupdate.conf <<EOF
services_blacklist="net.* sshd"
EOF
cat >/etc/periodic/daily/apk-autoupdate.sh <<EOF
#!/bin/sh
set -eu
apk-autoupdate
EOF
chmod 700 /etc/periodic/daily/apk-autoupdate.sh

#### Configuring SSH
#
# Change SSH to run on a different port
# This is not mandatory, but it does remove a bunch of garbage from your system logs
if ! grep -q "Port 6922" /etc/ssh/sshd_config; then
  echo "" >> /etc/ssh/sshd_config
  echo "Port 6922" >> /etc/ssh/sshd_config
fi

#### Configuring Tor
#

# Create directories for the hidden services
mkdir -p /var/lib/tor/services/admin /var/lib/tor/services/sacredground
chown -R tor /var/lib/tor
chmod 700 /var/lib/tor/services /var/lib/tor/services/admin /var/lib/tor/services/sacredground

# Log debug messages - Tor Project recommends against this as it may expose info about users, consider removing when finished configuring
cat >/etc/tor/torrc <<EOF
Log notice file /var/log/tor/notices.log
DataDirectory /var/lib/tor
%include /etc/tor/torrc.d/*.conf
EOF
mkdir -p /etc/tor/torrc.d/

# Create admin service that exposes SSH over port 22
cat >/etc/tor/torrc.d/admin.service.conf <<EOF
HiddenServiceDir /var/lib/tor/services/admin
HiddenServicePort 22 127.0.0.1:6922
EOF

# Create web service for the shrine
cat >/etc/tor/torrc.d/sacredground.service.conf <<EOF
HiddenServiceDir /var/lib/tor/services/sacredground
HiddenServicePort 80 127.69.69.1:8080
EOF

# rc-service tor restart

# Determine the onion service name for our shrine
SHRINEONION="$(cat /var/lib/tor/services/sacredground/hostname)"

#### Configuring nginx
#

# Disable the default nginx configuration
if test -e /etc/nginx/http.d/default.conf; then
  mv /etc/nginx/http.d/default.conf /etc/nginx/http.d/default.conf.disabled
fi

# Copy the default html directory to our clearnet directory
# This will let us test nginx before starting Tor
if ! test -e /var/lib/nginx/sacredground-clearnet; then
    cp -r /var/lib/nginx/html /var/lib/nginx/sacredground-clearnet
fi
# ... and will let us test nginx + Tor before uploading a real site
if ! test -e /var/lib/nginx/sacredground-onion; then
    cp -r /var/lib/nginx/html /var/lib/nginx/sacredground-onion
fi

# The try_files makes sure that http://asdf.onion/uri gets redirected to /uri/ and finds the index.html.
# absolute_redirect off makes sure that the redirect will be for a relative path like /uri/
# and not http://asdf.onion:8080/uri/.
# ... it would normally generate the :8080 for sacredground-onion and homemirror-onion
# because those sites listen on that port.
# This would cause a problem bc tor is proxying asdf.onion:80 -> localhost:8080.
# <https://serverfault.com/a/905740>

cat >/etc/nginx/http.d/sacredground-clearnet-http.conf <<EOF
server {
  listen $VPSIP4:80 default_server;
  listen [$VPSIP6]:80;
  server_name $VPSDNS;
  location /.well-known/acme-challenge {
    proxy_pass http://127.69.69.5:8080;
    proxy_set_header Host \$host;
  }
  location / {
    root /var/lib/nginx/sacredground-clearnet;
    return 301 https://\$host\$request_uri;
    try_files \$uri \$uri/ =404;
    add_header Onion-Location http://$SHRINEONION\$request_uri;
    error_page 404 /404.html;
  }
}
EOF

cat >/etc/nginx/http.d/sacredground-onion.conf <<EOF
server {
  listen 127.69.69.1:8080;
  server_name $SHRINEONION;
  location / {
    root /var/lib/nginx/sacredground-onion;
    absolute_redirect off;
    try_files \$uri \$uri/ =404;
    error_page 404 /404.html;
  }
}
EOF

# If the cert doesn't exist yet, create the HTTPS config file as disabled
# This prevents an error when nginx looks for a cert that doesn't exist
CLEARNET_HTTPS_CONF=/etc/nginx/http.d/sacredground-clearnet-https.conf
CLEARNET_HTTPS_CONF_DISABLED="$CLEARNET_HTTPS_CONF.disabled"
CLEARNET_HTTPS_CONF_TOWRITE="$CLEARNET_HTTPS_CONF"
if ! test -e "/etc/lego/certificates/$VPSDNS.crt"; then
  CLEARNET_HTTPS_CONF_TOWRITE="$CLEARNET_HTTPS_CONF_DISABLED"
fi
cat >"$CLEARNET_HTTPS_CONF_TOWRITE" <<EOF
server {
  listen $VPSIP4:443 ssl;
  listen [$VPSIP6]:443 ssl;
  server_name $VPSDNS;
  ssl_certificate /etc/lego/certificates/$VPSDNS.crt;
  ssl_certificate_key /etc/lego/certificates/$VPSDNS.key;
  location /.well-known/acme-challenge {
    proxy_pass http://127.69.69.5:8443;
    proxy_set_header Host \$host;
  }
  location / {
    root /var/lib/nginx/sacredground-clearnet;
    absolute_redirect off;
    try_files \$uri \$uri/ =404;
    add_header Onion-Location http://$SHRINEONION\$request_uri;
    error_page 404 /404.html;
  }
}
EOF

# Test the configuration
nginx -t

rc-service nginx restart

#### Configuring Let's Encrypt
#

# Create a user to run the lego command
if ! grep -q "^lego:" /etc/passwd; then
  adduser -D -G nginx lego
fi

# Create a directory for lego certificates
mkdir -p /etc/lego
chown lego:nginx /etc/lego
chmod 750 /etc/lego

# If we don't have any certificates yet, retrieve them
if ! test -e /etc/lego/certificates/$VPSDNS.crt; then
  su - lego -c "lego --domains='$VPSDNS' --email='$ACMEEMAIL' --accept-tos --path=/etc/lego --http --http.port=127.69.69.5:8080 run"
  chmod 640 /etc/lego/certificates/*
  mv "$CLEARNET_HTTPS_CONF_DISABLED" "$CLEARNET_HTTPS_CONF"
  nginx -t
  rc-service nginx restart
fi

# Add a cronjob to check every week if certs need to be renewed
# Lego will only actually renew them if they expire in 30 days or less
cat >/etc/periodic/weekly/lego.sh <<EOF
#!/bin/sh
set -eu
su - lego -c "/usr/bin/lego --domains='$VPSDNS' --email='$ACMEEMAIL' --accept-tos --path=/etc/lego --http --http.port=127.69.69.5:8080 --tls --tls.port=127.69.69.5:8443 renew"
chmod 640 /etc/lego/certificates/*
rc-service nginx reload
EOF
chmod 755 /etc/periodic/weekly/lego.sh

#### Finishing up...
#

echo "Tor local service names and onion names:"
for hname in /var/lib/tor/services/*/hostname; do
  echo "$hname :: $(cat $hname)"
done

echo "Let's Encrypt certificate expires:"
openssl x509 -enddate -noout -in /etc/lego/certificates/$VPSDNS.crt

Copy it to the VPS and then run it:

scp provision.sh sacredground.ip:/root/
ssh sacredground.ip sh /root/provision.sh

After running this script for the first time, you must change your ssh config for connecting to sacredground.ip, and add a Port directive:

### Connect to the VPS's public IP over Tor
Host sacredground.ip
  HostName 1.2.3.4
  Port 6922  ############################################ ADD THIS LINE
  User root
  IdentityFile ~/.ssh/njalla_ed25519
  ProxyCommand nc -x 127.0.0.1:9050 %h %p

You can also configure SSH to work with the .onion name. This may be faster and more privacy-preserving than using the IP address.

### Connect to the VPS's Onion address over Tor.
Host sacredground.onion
  HostName asdfqwerzxcv....onion
  User root
  IdentityFile ~/.ssh/njalla_ed25519
  ProxyCommand nc -x 127.0.0.1:9050 %h %p

Now you can do some new things:

Configuring HTTPS

To get a TLS certificate to enable HTTPS, we use Let’s Encrypt. This is provisioned automatically in the script.

You should probably make a backup so that you have a copy of your private key for Let’s Encrypt. The private key is stored in /etc/lego, and the example backup script will back up that location.

Backing up the VPS

Basically everything on the VPS originated on your local machine, and can be rebuilt by the scripts we use here. The exceptions are your Onion service keys, which determine your .onion domain name.

Depending on what you do with your site, you may have user-generated content like comments that you’d like to back up, or application databases or other state.

  1. You could consider cloud storage for this. The most basic version of that would be another VPS, perhaps from a provider other than Njalla in case they go down or your relationship with them deteriorates, and just using rsync to transmit files back and forth. I am unfortunately not aware of any companies that offer cloud storage like S3 which don’t require a link to an offline identity, however.
  2. You could copy that files you care about to your local machine. This means that any compromise of your local machine would link you to your Tor site.

In my case, I use target in my Makefile to create a backup tarball and copy it to my local machine.

backup target in Makefile
.PHONY: backup
backup: ## Make a backup and save it locally
	$(eval NOW=$(shell date +%Y%m%d-%H%M%S))
	mkdir -p backups/
	ssh "${SERVER}" "tar -f - -c /var/lib/tor/ /var/lib/gogs/ /etc/" | gzip > "backups/${SERVER}.backup.$(NOW).tar"

Vanity .onion names

This is purely optional. You can skip this section and use the .onion name that Tor generated for you when it started for the first time.

Many onion services have vanity names which start with a certain string. For instance, dark.fail’s Onion service is http://darkfailenbsdla5mal2mxn2uz66od5vtzd5qozslagrfzachha3f3id.onion/.

A tool called mkp224o can generate these. You have to compile it yourself. Some hardware can use special optimizations to make it faster.

You might run it like this, which attempts to generate several prefixes and stores the results in a directory called attempts/.

./configure --enable-intfilter=native
make
./mkp224o -d attemptes/ prefix1 prefix2 prefix3 ...

Shen Hong wrote a post about the Enterprise Onion Toolkit (clearnet, onionlink) which has more details on this tool, including optimizations. He calculates the expected time it would take to generate onion names with prefixes of various lengths, which may inform your decision on whether to pursue this yourself.

It’s also worth reading Tips when mining Onion Addresses by Alec Muffett, the author of the EOTK.

The results are directories named with the .onion service name and each containing three files:

You can copy this directory to /var/lib/tor/services/sacredground (which matches the location set in the torrc file by provision.sh), overwriting the Onion service hostname and keys that Tor generated when it started up for the first time.

Hugo

Hugo is a static site generator. It has few dependencies and executes completely on your own machine, making it a good fit for our purposes.

Building a Hugo website

If you’re completely new to Hugo, read the quickstart guide.

Take special note about the theme. Themse have significant privacy implications, since they control what code the client downloads, including requests for remote resources, tracking JavaScript, etc.

When I built this shrine, I had some unusual goals:

These goals are not necessary to build a good shrine, and if you don’t share them feel free to make different choices.

With those goals, I decided to build my own simple theme, called Onionskin. At the moment, it’s only available over Tor. See its readme for more details.

A short summary of its features:

Image Processing

We want to process all images locally, so that we cannot be tracked from e.g. a web application that we use to convert images for us. We also want to make sure that any photos we take with identifiable metadata are anonymized.

Local image processing

Screenshots can be resized with an ImageMagick command. This keeps the aspect ratio of the original image, and resizes it to fit inside the -resize 768x768 argument.

magick content/howto/extensions/gogs-first-run-orig.png -resize 768x768 content/howto/extensions/gogs-first-run.png

You can also use ImageMagick to convert an SVG to a favicon:

magick -density 32x32 -background transparent favicon.svg -define icon:auto-resize=32 -colors 4 favicon.ico

Removing identifying metadata

Photos taken on digital cameras (including phones) contain a lot of metadata, some of which may be used to identify the source. If you have photos that you wish to publish without connecting them to your offline identity, you should process them to make sure they don’t leak.

For this purpose, we use exiftran and exiftool. Both are in Homebrew and most Linux distribution package repositories.

image=./photo.jpg

# Rotate the bitmap so that you don't need the Orientation EXIF tag
exiftran -ia "$image"

# Remove all EXIF tags, except for ICC_Profile, which affects color
exiftool -overwrite_original_in_place -all= --icc_profile:all "$image"
Take extreme caution when publishing photos that were taken by you or someone connected to you. As geoguessr players have demonstrated, it is often very possible for a patient individual with access to only the public Internet to find the location an unremarkable photo was taken with high accuracy. If you aren’t convinced, watch some of what georainbolt does, especially his short Twitter videos.

Creating images from text

The Google Font to SVG Path webapp can convert any string of text to an SVG in any font on Google Fonts.

It’s open source and runs only in the browser; you can host it yourself by cloning the repository and running it behind any webserver (e.g. with Python cd google-font-to-svg-path; python3 -m http.server), although it still retrieves JavaScript and fonts from CDNs.

It was used for this site to create the logo and favicon.

Favicons

Favicons are not a requirement, but they are nice to have.

Inlining our favicon

One of our goals is that large pages can be retrieved by a single request. This means our favicon must be inlined into the page itself.

My Onionskin theme for Hugo can do this for us. It looks something like this:

{{- $faviconIco := resources.Get "favicon.ico" }}
<link href="data:image/x-icon;base64,{{ $faviconIco.Content | base64Encode }}" rel="icon" type="image/x-icon">
Getting a favicon

For more privacy, you can prioritize icon packs (like Twemoji and FontAwesome) and search through them locally.

Note that many of these sources require attribution.

Deploying

We can build the site locally with hugo, and then copy it to the VPS with rsync. rsync uses SSH to connect, and you can use name you specified in ~/.ssh/config for your VPS. Be careful to include the trailing slash on the source directory. Note that rsync’s -z argument compresses files before sending them, which makes an enormous difference over Tor, especially for the first deployment.

hugo mod get
hugo \
	--environment onion \
	--cleanDestinationDir \
	--ignoreCache \
	--printPathWarnings \
	--minify \
	--destination public/onion
hugo \
	--environment clearnet \
	--cleanDestinationDir \
	--ignoreCache \
	--printPathWarnings \
	--minify \
	--destination public/clearnet
rsync -zrvP --perms --chmod=ugo=rX --progress public/onion/  root@sacredground.ip:/var/lib/nginx/sacredground-onion/
rsync -zrvP --perms --chmod=ugo=rX --progress public/clearnet/  root@sacredground.ip:/var/lib/nginx/sacredground-clearnet/

I use a Makefile to do this so I can issue a single command make deploy-onion deploy-clearnet and it will do all of the above.

After you do this, you should see your new content when browsing to the onion address.

Tor and the modify/deploy/test cycle

If you’re restarting tor itself when deploying the site, the local tor service might be up but the network can’t find it for a few minutes after restarting it, with a message like it is likely that the service has changed its descriptor.

This happens when you restart tor, and occasionally other times too when the service has to reestablish a connection to tor.

You can try getting a new Tor identity (clearnet, onionlink) in Tor Browser, but you may just have to wait it out.

Extensions

This section showcases other things I did to build this shrine. They are less documented and more specific to my use case, but they might be a useful starting place for your own modifications.

Provisioning my extensions

I have a second provision script that must be run after the first one has run at least once.

provision-ext.sh code
#!/bin/sh
set -eu

# Note: this script expects that provision.sh has already run at least once successfully

apk update
apk add gogs gogs-openrc sqlite

#### Configure the home mirror
# This is for an Onion service with content from https://me.micahrl.com and https://com.micahrl.me

mkdir -p /var/lib/nginx/home-obverse
cat >/etc/nginx/http.d/home-obverse-onion.conf <<EOF
server {
  listen 127.69.69.2:8080;
  location / {
    root /var/lib/nginx/home-obverse;
    absolute_redirect off;
    try_files \$uri \$uri/ =404;
    error_page 404 /404.html;
  }
}
EOF
mkdir -p /var/lib/nginx/home-reverse
cat >/etc/nginx/http.d/home-reverse-onion.conf <<EOF
server {
  listen 127.69.69.6:8080;
  location / {
    root /var/lib/nginx/home-reverse;
    absolute_redirect off;
    try_files \$uri \$uri/ =404;
    error_page 404 /404.html;
  }
}
EOF

# Test the configuration
nginx -t

rc-service nginx restart

#### Configure Tor

# Create directories for the hidden services
mkdir -p /var/lib/tor/services/home-obverse /var/lib/tor/services/home-reverse /var/lib/tor/services/sacredgit
chown -R tor /var/lib/tor/services/home-obverse /var/lib/tor/services/home-reverse /var/lib/tor/services/sacredgit
chmod 700 /var/lib/tor/services/home-obverse /var/lib/tor/services/home-reverse /var/lib/tor/services/sacredgit

cat >/etc/tor/torrc.d/home-obverse.service.conf <<EOF
HiddenServiceDir /var/lib/tor/services/home-obverse
HiddenServicePort 80 127.69.69.2:8080
EOF

cat >/etc/tor/torrc.d/home-reverse.service.conf <<EOF
HiddenServiceDir /var/lib/tor/services/home-reverse
HiddenServicePort 80 127.69.69.6:8080
EOF

cat >/etc/tor/torrc.d/sacredgit.service.conf <<EOF
HiddenServiceDir /var/lib/tor/services/sacredgit
HiddenServicePort 80 127.69.69.3:3000
HiddenServicePort 22 127.69.69.3:3022
EOF

rc-service tor restart

#### Configure the Git server

SACREDGIT_ONION=$(cat /var/lib/tor/services/sacredgit/hostname)

if ! test -e /etc/gogs/conf/app.ini.dist; then
    cp /etc/gogs/conf/app.ini /etc/gogs/conf/app.ini.dist
fi

# We only set the config file if it doesn't have our revision key, which you can randomly generate out of band.
# This is because gogs overwrites its own config file when changes are made in the admin UI.
# If making changes here, make sure to first retrieve the latest config file from the remote system.
REVISION_KEY=LicoriceFlashyUnrentedWildlandParalyzeOverdress
if ! grep -q "$REVISION_KEY" /etc/gogs/conf/app.ini; then
    cat >/etc/gogs/conf/app.ini <<EOF
# REVISION_KEY=$REVISION_KEY

# Docs: https://github.com/gogs/gogs/blob/main/conf/app.ini
BRAND_NAME = SACRED REVISION CONTROL
RUN_USER   = gogs
RUN_MODE   = prod

[repository]
ROOT        = /var/lib/gogs/git
SCRIPT_TYPE = sh

[server]
APP_DATA_PATH    = /var/lib/gogs/data
STATIC_ROOT_PATH = /usr/share/webapps/gogs
EXTERNAL_URL     = http://$SACREDGIT_ONION/
DOMAIN           = $SACREDGIT_ONION
PROTOCOL         = http
HTTP_ADDR        = 127.69.69.3
HTTP_PORT        = 3000
SSH_DOMAIN       = $SACREDGIT_ONION
SSH_LISTEN_HOST  = 127.69.69.3
SSH_LISTEN_PORT  = 22
OFFLINE_MODE     = true
DISABLE_SSH      = false
SSH_PORT         = 3022
START_SSH_SERVER = true

[database]
DB_TYPE  = sqlite3
PATH     = /var/lib/gogs/db/gogs.db
SSL_MODE = disable
TYPE     = sqlite3
HOST     = 127.0.0.1:5432
NAME     = gogs
USER     = gogs
PASSWORD =

[security]
SECRET_KEY   = $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)
INSTALL_LOCK = true

[email]
ENABLED = false

[auth]
DISABLE_REGISTRATION = true

[session]
PROVIDER_CONFIG = /var/cache/gogs/sessions
PROVIDER        = file

[picture]
AVATAR_UPLOAD_PATH      = /var/lib/gogs/avatars
DISABLE_GRAVATAR        = true
ENABLE_FEDERATED_AVATAR = false

[attachment]
PATH = /var/lib/gogs/attachements

[log]
ROOT_PATH = /var/log/gogs
MODE      = file
LEVEL     = Info

[mailer]
ENABLED = false

[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL     = false
DISABLE_REGISTRATION   = true
ENABLE_CAPTCHA         = true
REQUIRE_SIGNIN_VIEW    = false
EOF
fi

# This will make administration easier
# Root can run 'rungogs ...' just like the gogs user can run 'gogs ...'.
# Without this you have to su to the 'gogs' user (sudo doesn't work)
# and export environment variables stored in /etc/conf.d/gogs (dot-sourcing it doesn't work).
cat >/usr/local/bin/rungogs <<EOF
#!/bin/sh
set -eu

# Get the variables from the Alpine gogs conf file
. /etc/conf.d/gogs

# Change to a directory that \$GOGS_USER will have access to
cd /tmp

echo "$(cat <<ENDSU

# Export all the vars found in the gogs environment file
# Without doing this,
set -a
. /etc/conf.d/gogs
set +a

# Actually run gogs
gogs \$@

ENDSU
)" | su -l \$GOGS_USER

EOF
chmod 700 /usr/local/bin/rungogs

rc-update add gogs default

# Changing the config file requires a restart
rc-service gogs restart




echo "Tor local service names and onion names:"
for hname in /var/lib/tor/services/*/hostname; do
  echo "$hname :: $(cat $hname)"
done

Makefile

I also use a Makefile for building and deploying the site.

Makefile
# This name must match the name you set up in your ~/.ssh/config to connect to the remote host over Tor
# You can set this to another value when running `make`, like `make SERVER=sacredground.onion`.
SERVER = sacredground.ip

.PHONY: help
help: ## Show this help
	@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

.PHONY: clearnet
clearnet: ## Build the static site for the clearnet environment
	hugo mod get
	hugo \
		--environment clearnet \
		--cleanDestinationDir \
		--ignoreCache \
		--printPathWarnings \
		--minify \
		--destination public/clearnet

.PHONE: deploy-clearnet
deploy-clearnet: clearnet ## Deploy the clearnet site
	time rsync \
		-zrvP \
		--perms \
		--chmod=ugo=rX \
		--progress \
		--delete \
		public/clearnet/ \
		${SERVER}:/var/lib/nginx/sacredground-clearnet/

# NOTE: We pass --minify to normalize source code, maybe making it a bit harder to identify the author
.PHONY: shrine
shrine: ## Build the static site for the shrine environment
	hugo mod get
	hugo \
		--environment onion \
		--cleanDestinationDir \
		--ignoreCache \
		--printPathWarnings \
		--minify \
		--destination public/onion


.PHONY: deploy-shrine
deploy-shrine: shrine ## Deploy the shrine site
	time rsync \
		-zrvP \
		--perms \
		--chmod=ugo=rX \
		--progress \
		--delete \
		public/onion/ \
		${SERVER}:/var/lib/nginx/sacredground-onion/

.PHONY: deploy-shrine-both
deploy-shrine-both: deploy-shrine deploy-clearnet ## Deploy both the onion and clearnet shrine

.PHONY: provision
provision: ## Provision the remote server
	scp ./content/howto/provision.sh ${SERVER}:/root/
	ssh ${SERVER} sh /root/provision.sh

.PHONY: provision-ext
provision-ext: ## Run provisioning extension script on the remote server
	scp ./content/howto/provision-ext.sh ${SERVER}:/root/
	ssh ${SERVER} sh /root/provision-ext.sh

# hugo mod clean, via <https://discourse.gohugo.io/t/hugo-theme-and-module-stopped-working-locally/38124/2>
.PHONY: clean
clean: ## Clean up anything that the deploy might have saved locally: generated HTML, Go mod cache, etc
	rm -rf public
	hugo mod clean --all

.PHONY: dev
dev: ## Run the site in dev mode
	hugo mod get
	hugo server \
		--buildDrafts \
		--buildFuture \
		--printPathWarnings \
		--bind 0.0.0.0

.PHONY: backup
backup: ## Make a backup and save it locally
	$(eval NOW=$(shell date +%Y%m%d-%H%M%S))
	mkdir -p backups/
	ssh "${SERVER}" "tar -c /var/lib/tor/services /var/lib/gogs/ /etc/ | bzip2" > "backups/${SERVER}.backup.$(NOW).tar.bzip2.incomplete"
	mv "backups/${SERVER}.backup.$(NOW).tar.bzip2.incomplete" "backups/${SERVER}.backup.$(NOW).tar.bzip2"

.PHONY: deploy-home-obverse-onion
deploy-home-obverse-onion: ## Deploy the home obverse (regular) onion site. Must have already been built.
	time rsync \
		-zrvP \
		--perms \
		--chmod=ugo=rX \
		--progress \
		--delete \
		../me.micahrl.com/public/onion-obverse/ \
		${SERVER}:/var/lib/nginx/home-obverse/

.PHONY: deploy-home-reverse-onion
deploy-home-reverse-onion: ## Deploy the home reverse (mirror) onion site. Must have already been built.
	time rsync \
		-zrvP \
		--perms \
		--chmod=ugo=rX \
		--progress \
		--delete \
		../me.micahrl.com/public/onion-reverse/ \
		${SERVER}:/var/lib/nginx/home-reverse/


.PHONY: deploy-home-onions
deploy-home-onions: deploy-home-obverse-onion deploy-home-reverse-onion ## Deploy already-built onion sites to the server

Running make by itself just prints a help message, which is generated automatically by the comments in the Makefile:

> make
backup               Make a backup and save it locally
clean                Clean up anything that the deploy might have saved locally: generated HTML, Go mod cache, etc
clearnet             Build the static site for the clearnet environment
deploy-onion         Deploy the onion site
dev                  Run the site in dev mode
help                 Show this help
onion                Build the static site for the onion environment
provision-ext        Run provisioning extension script on the remote server
provision            Provision the remote server

Configuring gogs

I deployed gogs to my onion service, so that I can publish the source code for the shrine and the source code for the theme.

After running provision-ext.sh, I can use Tor Browser to bring it up by connecting to the .onion address of my Gogs hidden service.

Screenshot of the gogs first-run page

Once the first-run configuration is complete:

To set up repositories and push to them:

Add another item to ~/.ssh/config. Note that we use the gogs user, NOT our username. (Similar to git-over-SSH GitHub, where we use git@github.com rather than our Github username.)

Host sacredgit.onion
  HostName asdfqwerzxcv....onion
  User gogs
  IdentityFile ~/.ssh/shrineadmin_ed25519
  ProxyCommand nc -x 127.0.0.1:9050 %h %p

Then you can run commands like

## Test that SSH works properly
ssh sacredgit.onion

## Add a git remote
git remote add sacredgit sacredgit.onion:shrineadmin/sacredground.git

## Push your local repo
git push -u sacredgit master

WARNING: if you are running this on your own machine, beware that your git commits may contain identifyable information! Make sure you’ve set user.name and user.email like:

git config user.name "Shrine Admin"
git config user.email "root@localhost"

Making a mistake here could cost you your anonymity. Mistakes like this are the major reason to use Tails or Qubes instead.

And make sure that all HISTORICAL commits also don’t contain any links to your offline identity. If you need to, you can change the author of historical commits by rewriting all commits in the repository. Make sure to back up your repo first, and then run a command like this:

git-filter-branch-example.sh
#!/bin/sh
set -eu

# Change every commit in this repo to be owned by the shrine admin

git filter-branch --env-filter '
  OLD_EMAIL="your-real-email-oops@example.com"
  NEW_EMAIL="root@localhost"
  NEW_NAME="Shrine Admin"

  if test "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL"
  then
    GIT_AUTHOR_EMAIL=$NEW_EMAIL
    GIT_AUTHOR_NAME=$NEW_NAME
  fi

  if test "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL"
  then
    GIT_COMMITTER_EMAIL=$NEW_EMAIL
    GIT_COMMITTER_NAME=$NEW_NAME
  fi
' -- --all

An Onion service for my site

I also generate an Onion service for my main website (clearnet, onionlink) and its mirror universe (clearnet, onionlink) making them accessible over Tor.

Appendix

Other references

Miscellaneous onion site resources

Onion site directories

This page is not intended to be a comprehensive list of Onion services. However, it can be useful to have a starting point for exploring what’s available on Tor.