Running a Tor Hidden Service
This guide assumes Lemmy has been installed using the official Docker Compose method.
Note that federation is not currently supported over the Tor network. An existing Lemmy instance is required. This procedure will proxy Lemmy though Tor, but federation tasks are still handled by HTTPS on the open internet.
Tor ("The Onion Router") is software designed to circumvent censorship and prevent bad actors from monitoring your activity on the internet by encrypting and distributing network traffic through a decentralized pool of relay servers run by volunteers all over the world.
A Tor hidden service is only accessible through the Tor network using the .onion
top-level domain with the official Tor Browser, or any client capable of communicating over a SOCKS5 proxy. Hosting a service on the Tor network is a good way to promote digital privacy and internet freedom.
Installing Tor
The official documentation suggests Ubuntu and Debian users install Tor from the deb.torproject.org
repository because it always provides the latest stable release of the software.
Administrative Access is Required
Commands below are expected to be executed as the root
user. To become root use sudo -i
or su -
.
With sudo
:
sudo -i
Password: [authenticate with current user password]
With su
:
su -
Password: [authenticate with root's password]
Note: To return to your account run: exit
.
Verify your architecture is supported
The package repository only supports amd64
, arm64
, and i386
architectures.
dpkg --print-architecture
If your architecture is not supported you may want to consider installing Tor from source.
Install prerequisite packages
apt install -y apt-transport-https ca-certificates gpg lsb-release wget
Enable the deb.torproject.org repository
Configure apt
to pull packages from deb.torproject.org
.
bash -c 'dist=$(lsb_release -s -c); /bin/echo -e \
"deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] \
https://deb.torproject.org/torproject.org $dist main\n\
deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] \
https://deb.torproject.org/torproject.org $dist main" \
> /etc/apt/sources.list.d/tor.list'
Import deb.torproject.org's GPG signing key
wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc \
| gpg --dearmor \
| tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null
The signing key ensures the package retrieved from the server was created by deb.torproject.org
.
Install tor
apt update && apt install -y tor
Creating a Tor hidden service
Create a new hidden service directory:
mkdir /var/lib/tor/hidden_lemmy_service
Append the following to /etc/tor/torrc
to tie the hidden service directory to the tor
daemon:
HiddenServiceDir /var/lib/tor/hidden_lemmy_service/
HiddenServicePort 80 127.0.0.1:10080
HiddenServiceDir [path]
is where tor
will store data related to the hidden service, and HiddenServicePort [hidden_service_port] [host_ip:port]
binds a port on the host to a hidden service port on the Tor network.
Enable and start the Tor daemon
systemctl enable --now tor
At startup tor
daemon will automatically populate /var/lib/tor/hidden_lemmy_service/
with encryption keys, certificates, and assign a hostname for the new service.
Determine your hidden service's hostname
cat /var/lib/tor/hidden_lemmy_service/hostname
The .onion
address contained in this file will be referred to as HIDDEN_SERVICE_ADDR
from here on.
Configure your existing Lemmy instance
Docker compose
Forward port 10080
from the proxy
container to the hidden service port 127.0.0.1:10080
. This exposes 10080/tcp
to the local host, and will not be directly accessible from the internet. For context "80:80"
binds port 80/tcp
(HTTP) to 0.0.0.0:80
on the host. Unless a firewall is configured to block incoming traffic to 80
this will be exposed to other hosts on the local area network (LAN) and/or the open internet.
docker-compose.yml
services:
# ...
proxy:
# ...
ports:
- "80:80"
- "443:443"
- "127.0.0.1:10080:10080"
Configure NGINX
Append a new server {...}
block to handle tor traffic, and add the Onion-Location
header to the SSL encrypted server exposed to the internet. This header informs Tor Browser users that an equivalent .onion
site exists on the Tor network by displaying an icon next to the address bar.
nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
# Original configuration listening on port 80
server {
listen 80;
# ...
}
# Original configuration listening on port 443
server {
listen 443;
# ...
location / {
# Handle Tor Browser's ".onion" link detection
add_header Onion-Location "http://HIDDEN_SERVICE_ADDR$request_uri" always;
# ...
}
}
# Establish a rate limit for the hidden service address
limit_req_zone $binary_remote_addr zone=HIDDEN_SERVICE_ADDR_ratelimit:10m rate=1r/s;
# Add tor-specific upstream aliases as a visual aid to
# avoid editing the incorrect server block in the future
upstream lemmy-tor {
server "lemmy:8536";
}
upstream lemmy-ui-tor {
server "lemmy-ui:1234";
}
# Add a copy of your current internet-facing configuration with
# "listen" and "server_listen" modified to send all traffic
# over the Tor network, incorporating the visual upstream aliases
# above.
server {
# Tell nginx to listen on the hidden service port
listen 10080;
# Set server_name to the contents of the file:
# /var/lib/tor/hidden_lemmy_service/hostname
server_name HIDDEN_SERVICE_ADDR;
# Hide nginx version
server_tokens off;
# Enable compression for JS/CSS/HTML bundle, for improved client load times.
# It might be nice to compress JSON, but leaving that out to protect against potential
# compression+encryption information leak attacks like BREACH.
gzip on;
gzip_types text/css application/javascript image/svg+xml;
gzip_vary on;
# Various content security headers
add_header Referrer-Policy "same-origin";
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block";
# Upload limit for pictrs
client_max_body_size 20M;
# frontend
location / {
# distinguish between ui requests and backend
# don't change lemmy-ui or lemmy here, they refer to the upstream definitions on top
set $proxpass "http://lemmy-ui-tor";
if ($http_accept = "application/activity+json") {
set $proxpass "http://lemmy-tor";
}
if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") {
set $proxpass "http://lemmy-tor";
}
if ($request_method = POST) {
set $proxpass "http://lemmy-tor";
}
proxy_pass $proxpass;
rewrite ^(.+)/+$ $1 permanent;
# Send actual client IP upstream
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# backend
location ~ ^/(api|feeds|nodeinfo|.well-known) {
proxy_pass "http://lemmy-tor";
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Rate limit
limit_req zone=HIDDEN_SERVICE_ADDR_ratelimit burst=30 nodelay;
# Add IP forwarding headers
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# pictrs only - for adding browser cache control.
location ~ ^/(pictrs) {
# allow browser cache, images never update, we can apply long term cache
expires 120d;
add_header Pragma "public";
add_header Cache-Control "public";
proxy_pass "http://lemmy-tor";
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Rate limit
limit_req zone=HIDDEN_SERVICE_ADDR_ratelimit burst=30 nodelay;
# Add IP forwarding headers
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Redirect pictshare images to pictrs
location ~ /pictshare/(.*)$ {
return 301 /pictrs/image/$1;
}
}
# ...
}
Apply the configuration(s)
Restart all services associated with your Lemmy instance:
docker compose down
docker compose up -d
Test connectivity over Tor
Using torsocks
, verify your hidden service is available on the Tor network.
torsocks curl -vI http://HIDDEN_SERVICE_ADDR
* Trying 127.*.*.*:80...
* Connected to HIDDEN_SERVICE_ADDR (127.*.*.*) port 80 (#0)
> HEAD / HTTP/1.1
> Host: HIDDEN_SERVICE_ADDR
> User-Agent: curl/7.76.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Server: nginx
Server: nginx
< Date: Wed, 07 Jun 2023 17:06:00 GMT
Date: Wed, 07 Jun 2023 17:06:00 GMT
< Content-Type: text/html; charset=utf-8
Content-Type: text/html; charset=utf-8
< Content-Length: 98487
Content-Length: 98487
< Connection: keep-alive
Connection: keep-alive
< Vary: Accept-Encoding
Vary: Accept-Encoding
< X-Powered-By: Express
X-Powered-By: Express
< Content-Security-Policy: default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *
Content-Security-Policy: default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *
< ETag: W/"180b7-EC9iFYAIlbnN8zHCayBwL3wAm64"
ETag: W/"180b7-EC9iFYAIlbnN8zHCayBwL3wAm64"
< Referrer-Policy: same-origin
Referrer-Policy: same-origin
< X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
X-Frame-Options: DENY
< X-XSS-Protection: 1; mode=block
X-XSS-Protection: 1; mode=block
<
* Connection #0 to host HIDDEN_SERVICE_ADDR left intact
Logging behavior
Hidden service traffic will appear to originate from the lemmyexternalproxy
docker network instead of an internet IP. Docker's default network address pool is 172.17.0.0/16
.
docker compose logs -f proxy
lemmy-proxy-1 | 172.*.0.1 - - # ...
lemmy-proxy-1 | 172.*.0.1 - - # ...
lemmy-proxy-1 | 172.*.0.1 - - # ...