Tor Onion Service on a VPS: Setup, Hardening and Honest Limits
Run a v3 Tor onion service on a Linux VPS: torrc config, nginx binding, client auth, PoW DoS defense, key backups — and what an onion address can't hide.
An onion service gives you a reachable address (something.onion) without exposing your server's IP address, without opening a single inbound port, and without a TLS certificate. That makes it useful for far more than hidden websites: internal admin panels, SSH access to servers behind CGNAT, private Git remotes, or a censorship-resistant mirror of a public site.
This guide covers a working v3 onion service on a Linux VPS, the hardening that actually matters, and — the part most tutorials skip — what an onion address does not protect you from.
Direct answer: the minimum working setup
On Debian or Ubuntu, three steps get you a live onion address:
apt update && apt install -y tor nginx
Bind your web server to loopback only. In /etc/nginx/sites-available/onion:
server {
listen 127.0.0.1:8080;
server_name _;
root /var/www/onion;
index index.html;
server_tokens off;
autoindex off;
access_log off;
}
Then add the service to /etc/tor/torrc:
HiddenServiceDir /var/lib/tor/web/
HiddenServicePort 80 127.0.0.1:8080
Reload and read the address:
systemctl reload tor
cat /var/lib/tor/web/hostname
You get a 56-character base32 address ending in .onion. Tor creates the directory with 0700 permissions owned by the tor user — do not change that, and do not chown it to root or your own user, or Tor will refuse to start.
Nothing needs to be open in your firewall. Inbound traffic arrives through Tor circuits that the daemon itself established outbound. That is the core property worth understanding: an onion service is a client of the Tor network, not a server listening on the internet.
How reachability works without open ports
Your Tor daemon picks a set of introduction points inside the network and publishes a signed descriptor to a distributed hash ring. A client fetches that descriptor, picks a rendezvous point, and both sides build circuits to it. Traffic is end-to-end encrypted and authenticated against the ed25519 key embedded in the onion address itself — there is no exit node in the path and no certificate authority involved.
Practical consequences:
- No IP exposure by design. Clients never learn your address; you never learn theirs.
- No CA, no cert renewal. The address is the public key. Self-signed TLS on top adds nothing except a warning in Tor Browser.
- Works behind NAT and CGNAT. Many people use onion services purely as a NAT punch-through for admin access, not for anonymity.
- Latency is high. Six hops. Expect a few hundred milliseconds of round-trip time and jitter, which is fine for HTTP and SSH and terrible for anything chatty or real-time.
Hardening the parts that actually leak
The Tor protocol is rarely the weak point. The application and the operating system around it are.
Bind everything to loopback
Verify nothing is listening on a public interface:
ss -tulpn | grep -v '127.0.0.1\|\[::1\]'
Anything on 0.0.0.0 is a correlation risk. If the same content is served on your public IP and via onion, a scanner can hash the response body, favicon, or TLS certificate and link the two. Mass internet scanning plus onion-address crawling makes that linkage trivial and automated.
Keep your firewall default-deny inbound anyway, so a misconfigured service does not silently become reachable:
nft add table inet filter
# or simply: ufw default deny incoming && ufw allow from 127.0.0.1
Strip identifying headers and error pages
Default nginx and Apache error pages, PHP X-Powered-By headers, and stack traces frequently embed the server hostname or internal IP. Set server_tokens off, disable PHP's expose_php, and configure your framework for production error handling. Also check any redirect logic: a hardcoded Location: header pointing at your clearnet domain instantly deanonymizes the service.
Kill outbound application calls
This is the most common real-world leak. Your onion site is fine, but the application behind it makes clearnet requests from the server's real IP:
- WordPress update checks, Gravatar, oEmbed, pingbacks
- Font, CDN, or analytics assets loaded server-side
- Webhooks, error reporting (Sentry), or license checks
- Outbound SMTP for password resets
- Cron jobs pulling remote feeds
None of these expose the server to visitors, but they do expose it to third parties who can be compelled to hand over logs, and they can be timing-correlated with onion activity. Audit with tcpdump -n -i eth0 'not port 22' on an idle server and see what talks.
Add client authorization for private services
If the service is meant for you or a small team, do not rely on the address being unguessable — descriptors are published, and an address can leak from a browser history, bookmark sync, or a referrer. Use v3 client auth. Each client generates an x25519 keypair; you place the public key on the server:
mkdir -p /var/lib/tor/web/authorized_clients
echo "descriptor:x25519:<CLIENT_PUBKEY_BASE32>" \
> /var/lib/tor/web/authorized_clients/alice.auth
systemctl reload tor
Clients without an authorized key cannot even decrypt the descriptor, so the service is invisible to them. This is a much stronger gate than an HTTP login page.
Turn on the PoW DoS defense
Onion services are cheap to attack: flooding introduction points costs an adversary almost nothing and can make your service unreachable. Modern Tor releases include a proof-of-work defense that forces clients to solve a puzzle when the service is under load:
HiddenServicePoWDefensesEnabled 1
It does not eliminate abuse, but it changes the economics considerably. Recent Tor versions also ship a lightweight vanguards layer by default to raise the cost of guard-discovery attacks against long-running services; if you run a high-value service, look at the full vanguards add-on from the Tor Project rather than assuming defaults are enough.
Back up the key, or lose the address
/var/lib/tor/web/hs_ed25519_secret_key is your identity. Lose it and the address is gone forever; leak it and someone else can impersonate your service. Copy it to encrypted offline storage, not to a Dropbox folder. If you want the key never to touch the server unencrypted at rest, note the trade-off honestly: full disk encryption on a VPS protects a seized offline disk, not a running machine whose keys are in RAM and whose hypervisor you do not control.
SSH over onion
A genuinely useful pattern: remove SSH from the public internet entirely.
Server side:
HiddenServiceDir /var/lib/tor/ssh/
HiddenServicePort 22 127.0.0.1:22
Client side, in ~/.ssh/config:
Host myvps
HostName <address>.onion
User admin
ProxyCommand nc -x 127.0.0.1:9050 -X 5 %h %p
Now port scans find nothing, brute-force logs go quiet, and you keep access even if the provider's IP is null-routed. Add client authorization so the SSH onion is not publicly discoverable. Keep one fallback access path — a provider console or a second onion — before you close port 22, and test it before you log out.
If you want lower latency for day-to-day administration and are fine with a fixed endpoint IP, a self-hosted WireGuard tunnel is the better tool. Onion for stealth and NAT traversal, WireGuard for speed.
Common mistakes
Running an onion service on the same server as an identifiable clearnet site. Shared logs, shared analytics, shared uptime patterns. If the two must coexist, at minimum use separate processes, separate vhosts on loopback, and no shared session or cache backend. Publishing an Onion-Location header on the clearnet site is fine — that is an intentional, public link, not a leak.
Assuming Tor hides the content from the host. Traffic is decrypted on your VPS. The provider's hypervisor can, in principle, read RAM and disk. Onion services protect the network location from clients and observers; they do nothing about the trust you place in the machine's operator.
Ignoring the clock. Descriptor publishing depends on time. If the VM clock drifts badly, the service becomes intermittently unreachable. Keep systemd-timesyncd or chrony running and check journalctl -u tor for descriptor upload errors.
Believing single-hop mode is a shortcut. HiddenServiceSingleHopMode (with HiddenServiceNonAnonymousMode) cuts latency by giving up server anonymity completely — the introduction points learn your IP. It is a legitimate choice for a public site that simply wants an onion entry point, and a catastrophic one if you need location privacy. Never enable it "for performance" without deciding which you want.
Treating slowness as a Tor problem. Circuit latency is unavoidable, but a starved node makes it far worse. Before blaming Tor, check whether your instance is fighting for CPU: the methods in detecting an oversold VPS apply directly, since Tor's crypto work is sensitive to steal time.
What an onion service does and does not protect
| Protects against | Does not protect against |
|---|---|
| Passive observers learning your server IP | A compromised application (RCE, SSRF, log leaks) |
| Port scanning and IP-based blocking | The hosting provider or anyone with hypervisor access |
| Certificate-authority trust issues | Content-based correlation with a clearnet mirror |
| Client IP exposure to your own service | Traffic-volume and uptime correlation over long periods |
| Casual discovery (with client auth) | Legal process against the provider or payment trail |
Two more honest points. First, a global adversary with visibility into large parts of the network can attempt traffic-confirmation attacks; onion services raise the cost substantially but do not make you invisible to a well-resourced, targeted investigation. Second, anonymity is a property of your whole workflow, not one config file. The billing trail is part of it: a server paid for with a KYC-verified card and administered from your home IP is linked to you regardless of how the service is reached. That is precisely why some operators pick a no-KYC VPS billed in Monero and never log in without a tunnel — it removes one identifying link in the chain. It does not remove the others, and no provider can promise otherwise.
FAQ
Do I still need HTTPS on an onion service?
No. The onion protocol already provides encryption and authenticates the server key. Add TLS only if you have a specific reason, such as an application that hard-requires https:// for secure cookies — and then expect certificate warnings unless you buy an onion-capable cert.
Can I get a readable address?
Partly. Tools like mkp224o brute-force ed25519 keys until the base32 output starts with a chosen prefix. Six to eight characters is realistic; a full word is not. Generate keys offline and copy them into HiddenServiceDir, then fix ownership to the tor user.
How do I run the same service on several servers? OnionBalance lets multiple backend instances share one onion address for redundancy and load distribution. Each backend runs its own onion service; the OnionBalance instance publishes a combined descriptor with the master key.
Will my onion address change after a reboot or reinstall?
Only if you lose HiddenServiceDir. Preserve that directory — including hs_ed25519_secret_key and its permissions — and the address is permanent.
Can visitors be blocked or rate-limited? Not by IP, since you never see one. Use application-level controls: authentication, per-session quotas, CAPTCHAs, and the PoW defense for circuit-level flooding.
Takeaway
The Tor part of an onion service takes five minutes; everything worth doing takes longer. Bind services to loopback, verify with ss that nothing else is exposed, audit outbound application traffic, add client authorization for private endpoints, enable the PoW defense, and back up the secret key offline. Then be clear with yourself about the threat model: an onion address hides where your server is from the people connecting to it — it does not hide what happens on the server, and it does not fix an identity leak that happened before the machine was ever provisioned.
