Home · Guides / 5 / Secure / 5.4
Guide 5.4
Onion service checklist
Guides 1–4 get a site online. This page is the onion-specific layer: current Tor packages, no extra TCP, surviving a disk failure, staying off the relay network, resisting floods, optional private access, and headers for a static tree like this one.
torrc option has changed.
Checklist
Boxes are ordinary HTML checkboxes: tick them while you work. They are not saved when you close the tab. Print the page if you want a paper copy.
1. Install Tor from the Tor Project
Ubuntu’s universe tor package has lagged behind security releases. Guide 1’s sudo apt install tor still works for a first test; a host you keep online should use the Tor Project repository.
-
Confirm a supported architecture (
amd64orarm64):sudo dpkg --print-architecture -
Install HTTPS transport and GnuPG, then the signing key published at support.torproject.org/apt/tor-deb-repo. Verify the fingerprint on that page before you trust the file.
sudo apt install apt-transport-https gnupg wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | sudo tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null -
Add the repo. Use your Ubuntu/Debian codename from
lsb_release -sc(for examplenobleorbookworm), not a guessed name:lsb_release -scOn modern APT (deb822), create
/etc/apt/sources.list.d/tor.sources:Types: deb deb-src URIs: https://deb.torproject.org/torproject.org/ Suites: noble Components: main Signed-By: /usr/share/keyrings/deb.torproject.org-keyring.gpgReplace
noblewith your codename. On older APT, use ator.listline with[signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg]instead. -
Install Tor and the keyring package so the signing key stays current:
sudo apt update sudo apt install tor deb.torproject.org-keyring tor --version
2. Unix socket instead of TCP
Guide 4 uses HiddenServicePort 80 127.0.0.1:80 and NGINX listen 127.0.0.1:80. That already keeps the site off the public internet. A unix socket goes one step further: there is no TCP port to mis-bind to 0.0.0.0.
-
Create a runtime directory that both NGINX and Tor can use.
www-dataowns the socket; groupdebian-tormay connect:sudo tee /etc/tmpfiles.d/onion-nginx.conf >/dev/null <<'EOF' d /run/onion 0750 www-data debian-tor - EOF sudo systemd-tmpfiles --create /etc/tmpfiles.d/onion-nginx.conf -
In the NGINX server block, listen on the socket instead of TCP 80:
server { listen unix:/run/onion/nginx.sock; server_name _; root /var/www/html; index index.html; } -
After NGINX starts, the socket must be group-readable by Tor. A drop-in keeps that after every restart:
sudo mkdir -p /etc/systemd/system/nginx.service.d sudo tee /etc/systemd/system/nginx.service.d/onion-socket.conf >/dev/null <<'EOF' [Service] ExecStartPost=/bin/chmod 660 /run/onion/nginx.sock ExecStartPost=/bin/chgrp debian-tor /run/onion/nginx.sock EOF sudo systemctl daemon-reload sudo systemctl restart nginx -
Point Tor at the same path (and remove the old TCP target):
HiddenServiceDir /var/lib/tor/hidden_service/ HiddenServicePort 80 unix:/run/onion/nginx.socksudo service tor restart
listen 80 and the onion target. Confirm with ss -nltp that nothing extra is on *:80.
3. Key permissions and backup
The onion address is derived from hs_ed25519_secret_key. Anyone with a copy of that file can publish as you. Anyone who loses the only copy cannot get the name back.
-
Confirm ownership and mode. Tor refuses to start if the directory is too open:
sudo ls -ld /var/lib/tor/hidden_service sudo ls -l /var/lib/tor/hidden_serviceYou want
debian-tor:debian-toranddrwx------(700) on the directory. Fix with:sudo chown -R debian-tor:debian-tor /var/lib/tor/hidden_service sudo chmod 700 /var/lib/tor/hidden_service sudo chmod 600 /var/lib/tor/hidden_service/* -
Copy the secret key, public key, and
hostnameto encrypted offline storage (a LUKS stick or similar). Do not email the secret key or leave it in world-readable cloud sync.sudo tar -C /var/lib/tor -czf /root/onion-hs-backup.tar.gz hidden_service sudo chmod 600 /root/onion-hs-backup.tar.gzMove that archive off the VPS, then delete the copy on the server once you have verified the backup.
-
If you generated a vanity address in guide 2, the mkp224o folder is a second copy of the same secret. Treat it like the live key: encrypt it or destroy it after install.
4. Do not run a relay on this host
A relay sees other people’s circuits. An onion service is a destination. Running both on one IP lets an observer correlate “this machine relays” with “this onion is reachable,” which is the opposite of isolation.
-
In
/etc/tor/torrc, leave relay lines commented. Do not setORPort,DirPort,ExitRelay, orBridgeRelayon this host.#ORPort 9001 #ExitRelay 0 -
Confirm Tor is only a client plus the onion service:
ss -nltp | grep torYou should see the SOCKS port (9050) and not an ORPort listener on 9001 or 443.
tor --hash-passwordis unrelated; skip it unless you use a control port with authentication.
5. Onion service DoS controls
Public onions get flooded at the introduction points and rendezvous, not only at NGINX. Client IP rate limits in NGINX usually see 127.0.0.1 (or the unix socket), so they cannot tell visitors apart. Use Tor’s defenses first; use NGINX as a global circuit breaker.
-
Check that your Tor build includes proof-of-work (0.4.8+ from the Tor repo, compiled with PoW):
tor --list-modulesYou want a line
pow: yes. Details and extra intro-point options are in the Tor DoS guidelines. -
Under the same
HiddenServiceDirblock intorrc:HiddenServicePoWDefensesEnabled 1 HiddenServiceEnableIntroDoSDefense 1 HiddenServiceEnableIntroDoSRatePerSec 25 HiddenServiceEnableIntroDoSBurstPerSec 200 HiddenServiceMaxStreams 16 HiddenServiceMaxStreamsCloseCircuit 1PoW asks overloaded clients to spend CPU before a rendezvous is built. Intro-point limits cap how fast introduction requests are accepted. Stream limits cap how many connections each rendezvous circuit may open.
-
Restart Tor, then optionally add a coarse NGINX limit that applies to the whole socket (all visitors share it):
limit_req_zone $server_name zone=onion:1m rate=30r/s; server { listen unix:/run/onion/nginx.sock; limit_req zone=onion burst=60 nodelay; }That will not stop a single heavy client as neatly as a clearnet per-IP limit. It can still shed load when the service is drowning. Caching static files (this site is already static) is the cheapest mitigation.
6. v3 client authorization
Use this only when the onion must not be public. After you add even one valid .auth file, everyone else is locked out, including people who already know the address. Skip it for this documentation site.
v3 auth is a pair of X25519 keys: the service stores the client’s public key; the client presents the private key. The procedure below is the one published by the
Tor Project
(needs OpenSSL 1.1+ and the basez package for base32 / base64pem).
-
On a trusted machine, generate a keypair:
sudo apt install openssl basez openssl genpkey -algorithm x25519 -out /tmp/client.prv.pem -
Export raw base32 public and private keys (32 bytes, no PEM wrapper):
cat /tmp/client.prv.pem | grep -v " PRIVATE KEY" | base64pem -d | tail --bytes=32 | base32 | sed 's/=//g' > /tmp/client.prv.key openssl pkey -in /tmp/client.prv.pem -pubout | grep -v " PUBLIC KEY" | base64pem -d | tail --bytes=32 | base32 | sed 's/=//g' > /tmp/client.pub.key -
On the service, create
/var/lib/tor/hidden_service/authorized_clients/alice.authwith one line:descriptor:x25519:PASTE_PUBLIC_KEY_BASE32_HEREThe file name can be anything ending in
.auth. One client per file. Then:sudo chown debian-tor:debian-tor /var/lib/tor/hidden_service/authorized_clients/*.auth sudo chmod 600 /var/lib/tor/hidden_service/authorized_clients/*.auth sudo service tor restart -
On the client, set
ClientOnionAuthDirintorrcand add a.auth_privatefile:ClientOnionAuthDir /var/lib/tor/onion_auth56CHARONIONADDRESSWITHOUTDOTONION:descriptor:x25519:PASTE_PRIVATE_KEY_BASE32_HERETor Browser can also paste the private key in its onion-auth UI, so visitors do not have to edit
torrc. Restart Tor after changing files. Removing a.authfile on the server revokes that client only after Tor restarts.
7. NGINX headers for a static site
This repository is HTML and CSS only. Do not enable PHP or MySQL for it. Onion transport is already encrypted, so TLS certificates and HSTS are unnecessary on a pure .onion vhost.
-
In the server block that serves these files:
server { listen unix:/run/onion/nginx.sock; root /var/www/html; index index.html; server_tokens off; autoindex off; gzip on; gzip_types text/plain text/css image/svg+xml; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer" always; add_header X-Frame-Options "DENY" always; add_header Content-Security-Policy "default-src 'self'; img-src 'self'; style-src 'self'; script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'" always; location / { try_files $uri $uri/ =404; } }script-src 'none'matches this site (no JavaScript). If you later add a script, that header will block it until you change the policy. -
Test and reload:
sudo nginx -t sudo service nginx reload