The mental model

Three actors:

  1. Your machine (the service host) — runs Tor, exports a hidden service definition pointing at a local port.
  2. Tor network — uses cryptographic rendezvous to connect clients to the service without revealing either side's IP.
  3. Client — uses a Tor-aware client (Tor Browser, Tor SOCKS proxy, etc.) to look up the .onion + connect.

The .onion address is the service's public key. Clients verify the key by name; no certificate authority needed. End-to-end encrypted by construction.

Install Tor

# Debian / Ubuntu
sudo apt install tor

# Fedora / RHEL
sudo dnf install tor

# Verify
sudo systemctl status tor

Configure an onion service

Edit /etc/tor/torrc:

# A v3 onion service (the modern format; 56-character addresses)
HiddenServiceDir /var/lib/tor/my-service/
HiddenServiceVersion 3
HiddenServicePort 80 127.0.0.1:8080
HiddenServicePort 22 127.0.0.1:22

# Optional: hardening
HiddenServiceMaxStreams 100
HiddenServiceMaxStreamsCloseCircuit 1

Reload Tor:

sudo systemctl restart tor

# Read the generated address
sudo cat /var/lib/tor/my-service/hostname
# abcdef...<56 chars>.onion

That's the address. Anyone with the Tor Browser can visit http://<56-char>.onion and reach the local service on port 8080.

Common use cases

Expose a self-hosted service from behind CGNAT

# Your home server runs (e.g.) Forgejo on 127.0.0.1:3000.
# Add to /etc/tor/torrc:
HiddenServiceDir /var/lib/tor/git/
HiddenServiceVersion 3
HiddenServicePort 443 127.0.0.1:3000

# Reload Tor; read the .onion; share it with collaborators
# They use Tor Browser, get a working HTTPS-like experience.

No port forwarding; no public IP needed; works through residential ISP CGNAT.

SSH-over-onion for management

# Server torrc
HiddenServiceDir /var/lib/tor/ssh/
HiddenServiceVersion 3
HiddenServicePort 22 127.0.0.1:22

# Restart, read hostname
# Then ssh on the client:
torify ssh root@<56-char>.onion
# Or with Tor SOCKS proxy via netcat
ssh -o ProxyCommand="nc -X 5 -x 127.0.0.1:9050 %h %p" root@<56-char>.onion

Remote SSH access without exposing port 22 to the internet at all. Useful for emergency management of remote boxes.

Anonymous publishing / whistleblowing

For information whose source needs protection, the SecureDrop project (used by many newsrooms) is built on Tor onion services. Less relevant for typical homelab but the canonical "I need to receive submissions without identifying the sender" pattern.

Vanity addresses (optional)

Onion addresses are random by default. The mkp224o tool brute-forces partial-prefix matches:

sudo apt install build-essential automake libsodium-dev
git clone https://github.com/cathugger/mkp224o
cd mkp224o && ./autogen.sh && ./configure && make

# Try ~10 minutes for "amir" prefix (4 chars)
./mkp224o -d ./generated amir

# Copy the generated key to /var/lib/tor/my-service/ (replace existing)
sudo cp -r generated/<match>/* /var/lib/tor/my-service/
sudo chown -R debian-tor:debian-tor /var/lib/tor/my-service/
sudo chmod 700 /var/lib/tor/my-service/
sudo systemctl restart tor

Longer prefixes take exponentially more time. amir-something.onion (10 chars) is feasible; "perfectvanity.onion" full match would take eons.

Client auth (private onion services)

By default, anyone who knows the address can connect. For "only my friends can reach this":

# Generate a client keypair (offline)
openssl genpkey -algorithm x25519 -out priv.pem
openssl pkey -in priv.pem -pubout -out pub.pem

# Convert to base32 (the format Tor wants)
PUBKEY=$(grep -v '^---' pub.pem | base64 -d | tail -c 32 | base32 | sed 's/=//g')

# Add the public key to the service config
echo "descriptor:x25519:$PUBKEY" | sudo tee /var/lib/tor/my-service/authorized_clients/friend1.auth

# Client side: configure torrc with the private key
# ClientOnionAuthDir /var/lib/tor/onion_auth/
# echo "<onion-without-suffix>:descriptor:x25519:<privkey>" > /var/lib/tor/onion_auth/friend1.auth_private

Without the matching private key, connections fail at the rendezvous stage. The .onion is "shared but auth-gated."

Worth knowing

  • Latency. Onion services route through 6 Tor relays (3 for the client, 3 for the service); RTT is 200-500ms typical. Fine for SSH / git / web; awful for streaming media.
  • Throughput. A few Mbps; constrained by Tor's network. Don't expect to host large downloads.
  • Always-on uptime. The service registers periodically with introduction points; if your Tor process is restarted, brief unreachability follows. Stable hosts give best UX.
  • Backup the private key. The hs_ed25519_secret_key in the HiddenServiceDir IS your .onion address. Lose it, and that .onion is gone forever (the address is derived from this key).
  • Don't run an exit relay accidentally. Onion services are about you offering a service, not your machine relaying others' anonymous traffic. Different config; different risks (exit relays attract legal attention).

Running an actual Tor relay (not an onion service)

A Tor relay forwards anonymous traffic for other users; helps Tor's bandwidth pool. Different from onion services. Adding a relay to a homelab:

# /etc/tor/torrc
SocksPort 0           # not a SOCKS client
ORPort 9001           # relay listener (TCP); open in firewall
Nickname myhomelab
ContactInfo <email>
RelayBandwidthRate 5 MBytes
RelayBandwidthBurst 10 MBytes
ExitRelay 0           # NOT an exit relay
Exit 0
DirPort 9030          # optional

Restart Tor. The relay appears in the Tor network within ~24 hours after measurement. Run as a non-exit relay (very low legal risk); exit relays are a different story and not for typical homelab.

When onion services aren't the right tool

  • For sub-second latency, use Cloudflare Tunnels (see that tutorial), NetBird (see that tutorial), or Tailscale.
  • For "I want a normal-looking domain," DNS + a real public IP / proxy is needed.
  • For high-throughput services, Tor's per-circuit bandwidth is limiting.

For "expose this service without compromising location, without DNS, without router config, and over a privacy-respecting protocol," Tor onion services remain unique.