A v3 .onion hidden service is the modern way to expose a TCP service over Tor without revealing the server's IP. The address is a 56-character base32 fingerprint of an ed25519 public key, ending in .onion. The setup is three lines of torrc plus a daemon reload — but the operational details (key backup, file permissions, advertising the service from your clearnet site) are where most first deployments go wrong. Here's the complete walkthrough, including the single-onion fast path and the failure modes that cost addresses.
What you need
- A server with the Tor daemon installed.
- A local service the .onion will proxy to (a webserver on
127.0.0.1:8080, an SSH daemon, anything TCP). - Root or sudo access to edit
torrcand the Tor data directory.
v3 onion has been the only supported version since Tor 0.4.6 (2021). If your distro's Tor is older than that, upgrade first — v2 is gone and won't come back.
Step 1: define the service in torrc
Add this block to your torrc (location depends on install — /etc/tor/torrc on Debian/Ubuntu):
HiddenServiceDir /var/lib/tor/myservice/
HiddenServicePort 80 127.0.0.1:8080HiddenServiceDir is where Tor will store the .onion private key and the public hostname for this service. It must be a path the tor user can read and write, and must have permissions 700 (owner-only). Tor will refuse to start if it's 755 or world-readable.
HiddenServicePort is the mapping: public onion port (left) to local backend (right). One line per port. To also expose HTTPS:
HiddenServiceDir /var/lib/tor/myservice/
HiddenServicePort 80 127.0.0.1:8080
HiddenServicePort 443 127.0.0.1:8443For an SSH-over-onion service:
HiddenServiceDir /var/lib/tor/ssh-onion/
HiddenServicePort 22 127.0.0.1:22Each HiddenServiceDir defines one onion address. To run two distinct services with two different .onion addresses, declare two blocks with different directories.
Step 2: reload Tor and read the address
sudo systemctl reload tor
# or
sudo pkill -HUP tor
# Tor creates the directory on first start. Wait a few seconds, then:
sudo cat /var/lib/tor/myservice/hostname
# myveryunlikelyloockingvanityaddrthatlls45thinglooksokayyhotya2id.onionThat .onion string is the public address. There's no DNS to register, no certificate to buy, no port to forward. It just works — provided your local service on 127.0.0.1:8080 is actually responding.
Test from another machine that has Tor:
torsocks curl -v http://myveryunlikely...id.onion/Or open it in Tor Browser.
Step 3: back up the private key
This is the step people skip and then bitterly regret. The .onion address is derived from the keypair in:
/var/lib/tor/myservice/
├── authorized_clients/ # for client authorization, optional
├── hostname # the .onion address (public, derived)
├── hs_ed25519_public_key # public key (derivable from private)
└── hs_ed25519_secret_key # PRIVATE KEY — losing this loses the address forever
Back up hs_ed25519_secret_key somewhere safe. Anyone with that file can impersonate your .onion address from any machine. Treat it like an SSH host key, but with no recovery path.
A reasonable backup workflow:
# Encrypted, single-file, off-host
sudo tar czf - -C /var/lib/tor/myservice/ . \
| gpg --symmetric --cipher-algo AES256 \
> myservice-onion-backup-$(date +%F).tar.gz.gpgMove the resulting file to a different host or cold storage. If your server dies tomorrow, you can recreate the .onion address by un-encrypting the backup, dropping it into HiddenServiceDir on a new machine, and reloading Tor.
Permissions checklist
Tor is paranoid about file permissions on hidden service data. If the daemon fails to start, this is usually why:
sudo ls -la /var/lib/tor/myservice/
# Should show:
# drwx------ 2 debian-tor debian-tor ... .
# drwxr-xr-x 5 debian-tor debian-tor ... ..
# -rw------- 1 debian-tor debian-tor ... hs_ed25519_public_key
# -rw------- 1 debian-tor debian-tor ... hs_ed25519_secret_key
# -rw------- 1 debian-tor debian-tor ... hostnameThe directory must be 700, files must be 600, all owned by the tor user (named debian-tor on Debian, _tor on macOS, tor on Fedora). Fix with:
sudo chmod 700 /var/lib/tor/myservice
sudo chmod 600 /var/lib/tor/myservice/*
sudo chown -R debian-tor:debian-tor /var/lib/tor/myserviceAdvertising from clearnet: the Onion-Location header
If you also have a clearnet site, Tor Browser will auto-suggest the .onion equivalent when it sees the Onion-Location header. Set it in your reverse proxy:
# nginx
add_header Onion-Location "http://myveryunlikely...id.onion$request_uri" always;# Apache
Header set Onion-Location "http://myveryunlikely...id.onion%{REQUEST_URI}s"When a Tor Browser user visits the clearnet site, the address bar shows a .onion badge offering to switch. This is the recommended cross-link pattern — see the Tor Project's onion-location docs for the spec.
Single-onion mode (faster, no service-side anonymity)
The default v3 onion routes both client and service through 3-hop circuits, giving anonymity to both ends. For high-traffic public services where the operator's location is already known (a corporate onion, a public news outlet's mirror), single-onion mode cuts the service-side circuit to a single hop:
HiddenServiceDir /var/lib/tor/myservice/
HiddenServicePort 80 127.0.0.1:8080
HiddenServiceSingleHopMode 1
HiddenServiceNonAnonymousMode 1Both lines are required (and both must appear in torrc, not via SETCONF). The trade-off is explicit: faster, simpler, more reliable connections, but the .onion no longer hides where the server is. Clients still get full anonymity — that side hasn't changed.
Single-onion is the right mode for: high-traffic public mirrors (Facebook's facebookcorewwwi.onion-style), CDN-style content where geographic origin isn't sensitive, internal services where you control all the clients.
Operational tuning
For services that need to scale:
HiddenServiceDir /var/lib/tor/myservice/
HiddenServicePort 80 127.0.0.1:8080
# More introduction points = more resilient to one going down,
# more rendezvous traffic. Default is 3; max useful is around 10.
HiddenServiceNumIntroductionPoints 6
# Cap streams per circuit to mitigate burst abuse
HiddenServiceMaxStreams 50
HiddenServiceMaxStreamsCloseCircuit 1
# Don't lazily build new circuits between requests
HiddenServiceUseStrictGuards 1HiddenServiceMaxStreamsCloseCircuit 1 causes Tor to drop the circuit (rather than just the offending stream) when a client exceeds HiddenServiceMaxStreams. Useful for cheap denial-of-service mitigation.
Vanity .onion addresses with mkp224o
A v3 onion is 56 base32 characters of an ed25519 public key fingerprint. You can grind the keypair until the address starts with a chosen prefix:
# Build mkp224o once
git clone https://github.com/cathugger/mkp224o
cd mkp224o
./autogen.sh && ./configure && make
# Grind for an address starting with "techea"
./mkp224o -d ./output techeaEach character of prefix multiplies the search space by 32. Three-character prefixes complete in seconds. Six characters take minutes to hours on a fast CPU. Eight characters and beyond want a GPU or a server farm.
Once mkp224o finds a match, it writes a directory containing hs_ed25519_secret_key, hs_ed25519_public_key, and hostname — drop the whole directory into your HiddenServiceDir, reload Tor, and the vanity address is live.
Client authorization (private onion)
To restrict access to a list of pre-shared clients (a private onion):
HiddenServiceDir /var/lib/tor/myservice/
HiddenServicePort 80 127.0.0.1:8080
HiddenServiceAuthorizeClient v3-stealthYou generate a per-client x25519 keypair, install the public key in HiddenServiceDir/authorized_clients/clientname.auth, and the client adds the private key to their own torrc:
ClientOnionAuthDir /var/lib/tor/onion_authWith a matching .auth_private file in that directory. Without the key, the .onion address is unreachable — descriptors don't even publish to the consensus.
What to monitor
A live hidden service should be visible in the Tor consensus once descriptors propagate (60-90 seconds after start). Check with nyx:
sudo nyx
# press 'i' for the connection list
# you should see HSRendezvous and HSIntro circuitsIf you see only client circuits and no HS* entries minutes after starting, the service didn't register — usually a permissions problem or torrc syntax error. Check journalctl -u tor for errors.





