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.

Sources Steps follow the Tor Project’s public operator docs: Debian/Ubuntu Tor packages, onion service DoS guidelines, and v3 client authorization. Check those pages if a package name or 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.

  1. Confirm a supported architecture (amd64 or arm64):

    sudo dpkg --print-architecture
  2. 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
  3. Add the repo. Use your Ubuntu/Debian codename from lsb_release -sc (for example noble or bookworm), not a guessed name:

    lsb_release -sc

    On 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.gpg

    Replace noble with your codename. On older APT, use a tor.list line with [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] instead.

  4. 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.

  1. Create a runtime directory that both NGINX and Tor can use. www-data owns the socket; group debian-tor may 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
  2. 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;
    }
  3. 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
  4. Point Tor at the same path (and remove the old TCP target):

    HiddenServiceDir /var/lib/tor/hidden_service/
    HiddenServicePort 80 unix:/run/onion/nginx.sock
    sudo service tor restart
If this feels fragile Localhost TCP from guide 4 is still acceptable. Do not mix both a public 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.

  1. 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_service

    You want debian-tor:debian-tor and drwx------ (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/*
  2. Copy the secret key, public key, and hostname to 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.gz

    Move that archive off the VPS, then delete the copy on the server once you have verified the backup.

  3. 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.

  1. In /etc/tor/torrc, leave relay lines commented. Do not set ORPort, DirPort, ExitRelay, or BridgeRelay on this host.

    #ORPort 9001
    #ExitRelay 0
  2. Confirm Tor is only a client plus the onion service:

    ss -nltp | grep tor

    You should see the SOCKS port (9050) and not an ORPort listener on 9001 or 443. tor --hash-password is 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.

  1. Check that your Tor build includes proof-of-work (0.4.8+ from the Tor repo, compiled with PoW):

    tor --list-modules

    You want a line pow: yes. Details and extra intro-point options are in the Tor DoS guidelines.

  2. Under the same HiddenServiceDir block in torrc:

    HiddenServicePoWDefensesEnabled 1
    HiddenServiceEnableIntroDoSDefense 1
    HiddenServiceEnableIntroDoSRatePerSec 25
    HiddenServiceEnableIntroDoSBurstPerSec 200
    HiddenServiceMaxStreams 16
    HiddenServiceMaxStreamsCloseCircuit 1

    PoW 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.

  3. 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.

Captchas on onion Third-party captcha APIs send visitors to the clearnet and often need JavaScript. They are a poor fit for a no-JS onion docs site. Prefer Tor PoW and keeping the site static.

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).

  1. On a trusted machine, generate a keypair:

    sudo apt install openssl basez
    openssl genpkey -algorithm x25519 -out /tmp/client.prv.pem
  2. 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
  3. On the service, create /var/lib/tor/hidden_service/authorized_clients/alice.auth with one line:

    descriptor:x25519:PASTE_PUBLIC_KEY_BASE32_HERE

    The 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
  4. On the client, set ClientOnionAuthDir in torrc and add a .auth_private file:

    ClientOnionAuthDir /var/lib/tor/onion_auth
    56CHARONIONADDRESSWITHOUTDOTONION:descriptor:x25519:PASTE_PRIVATE_KEY_BASE32_HERE

    Tor 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 .auth file 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.

  1. 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.

  2. Test and reload:

    sudo nginx -t
    sudo service nginx reload
Efficiency on this static tree Gzip (or Brotli, if your NGINX build has it) is enough. There is no application cache to tune. If you want smaller pages, compress the terminal screenshots; they are the bulk of the bytes.