SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-13

SSH Through Tor Onion Service Without Open Ports

Put sshd behind a Tor onion service, close every inbound port, and avoid lockout. See v3 client authorisation, setup order, and recovery via VNC or serial console.

Wetin SSH over Tor onion service dey change

SSH over Tor onion service let you administer VPS wey no dey accept inbound connection for any port. The server dey dial out to Tor network and keep that connection open. Your SSH session dey come back through the same connection, so nothing need listen on the public IP address.

The effect for the log dey show immediately. A machine wey get public SSH port fit collect thousands of failed password attempts every day from scanners. Move sshd behind an onion service and block inbound traffic for the firewall, then /var/log/auth.log go record only the sessions wey you start.

The cost be say tor dey inside the path of every admin session. Na userspace daemon wey must start and bootstrap after every reboot before you fit log in. Plan for this before you close the port, because the failure mode here na losing access to machine wey you no fit reach physically.

Get way to enter again before you touch anything

No start until you get recovery path wey no use SSH.

Open your provider console now, the VNC or serial console for the control panel, then log in with am. If you no know the root password, reset the root password from the panel first and confirm say e dey work. Console wey you never test no be recovery path.

The order below matter. Each step must work before the next one run, and port 22 go remain open until the onion route dey work.

  1. Install tor and confirm say e bootstrap.
  2. Define the onion service and read the address.
  3. Connect through the onion while port 22 still dey open.
  4. Add client authorisation, then connect again.
  5. Bind sshd to loopback and close port 22.
  6. Reboot, then connect through the onion again.

Keep your current SSH session open throughout the whole process. An established session fit survive firewall change wey go block new connection, so na your first rescue line.

Install tor for server

Ubuntu get tor for im repository, but that build dey often old. The Tor Project repository get the version wey their documentation describe. Add am with the commands from their apt repository guide.

sudo apt update
sudo apt install -y apt-transport-https wget gpg
wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | sudo tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null

Write /etc/apt/sources.list.d/tor.sources. Suites go collect your release codename, wey lsb_release -cs go print (noble for Ubuntu 24.04).

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
sudo apt update
sudo apt install -y tor deb.torproject.org-keyring
sudo journalctl -u tor@default -n 20 --no-pager

The log suppose end with Bootstrapped 100% (done). If e stop below that point, tor no fit reach network. Almost every time, na outbound firewall rule or clock wey wrong badly cause am.

The unit name na trap. systemctl status tor report Active: active (exited) even when everything dey healthy, because Debian and Ubuntu package tor as multi-instance master unit. The only work of this unit na to bring in the real instance. The daemon itself dey run as tor@default.service. Use that name for status and journalctl. Start, stop and reload of tor still reach the instance, so sudo systemctl reload tor go work as you expect.

Define the onion service for port 22

Add two lines to /etc/tor/torrc.

HiddenServiceDir /var/lib/tor/ssh/
HiddenServicePort 22 127.0.0.1:22

The second line tell tor make e accept virtual port 22 for the onion address, then connect to 127.0.0.1:22 for the box. Tor dey reach sshd through loopback. Na this make sshd fit later stop listening on the public address.

sudo systemctl reload tor
sudo cat /var/lib/tor/ssh/hostname

That one go print 56 base32 characters, followed by .onion. Those characters na the service public key for encoded form. No certificate authority or name registration dey involved for this setup.

Make tor create /var/lib/tor/ssh/ by itself. If you create am manually with wrong owner or permission wey looser pass 0700, tor no go use am. The journal go report say the directory too permissive. The files inside na the service identity: hs_ed25519_secret_key na the address. Back up that directory with mode 600, and keep the copy outside the box. If you lose am, you go get new address and need edit config for every client.

Connect from your workstation

Your workstation need a tor client, and e no need any configuration. For Debian or Ubuntu, na sudo apt install -y tor netcat-openbsd. Tor go listen on 127.0.0.1:9050 as SOCKS5 proxy. SOCKS na general proxy protocol. Version 5 fit carry hostname instead of IP address. Na this part matter here.

OpenSSH no get SOCKS client by itself, so helper program go make the connection. Add this to ~/.ssh/config.

Host myvps
  HostName xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion
  User admin
  ProxyCommand /usr/bin/nc -X 5 -x 127.0.0.1:9050 %h %p
  ServerAliveInterval 30

-X 5 select SOCKS5, while -x 127.0.0.1:9050 point to the local tor. %h send the onion name to tor as name, so tor go resolve am inside the network. You must use the OpenBSD netcat for this. GNU netcat no get -X option, and e go stop with nc: invalid option -- 'X'.

ssh myvps

The first connection slow because tor dey build circuit before anything else happen. Accept the host key fingerprint the same way you go do am anywhere else. From here, the usual SSH key handling still apply without change. Na the transport change. The authentication no change.

For one-time connection, you fit skip the config entry: torsocks ssh admin@xxxxx.onion do the same work.

Add v3 client authorisation

As e dey now, anybody wey learn the address fit reach your SSH banner and start guessing. Onion addresses no fit be listed from the directory system, so the address dey behave like secret, but e fit leak through normal ways: shell history and config files wey dem commit to git repository. Client authorisation close this gap. The service publish its descriptor encrypted to client key, so person wey get the address but no key no fit even locate the service.

Generate x25519 key pair for the client. This na the pipeline from Tor Project client authorisation guide, with one change.

openssl genpkey -algorithm x25519 -out /tmp/k1.prv.pem
grep -v " PRIVATE KEY" /tmp/k1.prv.pem | base64 -d | tail --bytes=32 | base32 | sed 's/=//g' > /tmp/k1.prv.key
openssl pkey -in /tmp/k1.prv.pem -pubout | grep -v " PUBLIC KEY" | base64 -d | tail --bytes=32 | base32 | sed 's/=//g' > /tmp/k1.pub.key

The published version of those lines use base64pem -d, but stock Ubuntu install no get am. The command then stop with base64pem: command not found. GNU base64 -d decode the same PEM body, so use am instead.

For the server, install the public key.

sudo install -d -m 700 -o debian-tor -g debian-tor /var/lib/tor/ssh/authorized_clients
echo "descriptor:x25519:PASTE_PUBLIC_KEY_HERE" | sudo tee /var/lib/tor/ssh/authorized_clients/laptop.auth >/dev/null
sudo chown debian-tor:debian-tor /var/lib/tor/ssh/authorized_clients/laptop.auth
sudo systemctl reload tor

Na only files wey end with .auth dem dey read. Save am as laptop.auth.txt. If you save am with no filename extension, tor go ignore the file without printing any error, and the service go quietly remain open to anybody wey get the address.

For the client, install the private key. For Ubuntu, tor daemon dey run as debian-tor user and e no fit read files inside your home directory, so keep the directory for place wey that user fit reach.

sudo install -d -m 700 -o debian-tor -g debian-tor /var/lib/tor/onion_auth
echo "ADDRESS_WITHOUT_DOT_ONION:descriptor:x25519:PASTE_PRIVATE_KEY_HERE" | sudo tee /var/lib/tor/onion_auth/myvps.auth_private >/dev/null
sudo chown debian-tor:debian-tor /var/lib/tor/onion_auth/myvps.auth_private
sudo chmod 600 /var/lib/tor/onion_auth/myvps.auth_private

Add ClientOnionAuthDir /var/lib/tor/onion_auth to the client's /etc/tor/torrc, then reload tor. If you dey run tor as your own user instead, like the Homebrew build for macOS, point ClientOnionAuthDir to ~/.tor/onion_auth with mode 0700.

The address inside that file na the 56 characters without .onion suffix. Delete /tmp/k1.prv.pem and /tmp/k1.prv.key when you don finish.

Now test both directions. ssh myvps suppose still connect. From machine wey no hold any key, the same address suppose fail. That failure prove say the authorisation dey active.

Close port 22, for this order

Set safety net first. This one command go undo both changes wey dey below after fifteen minutes if you lock yourself out.

sudo systemd-run --on-active=15m --unit=ssh-rescue \
  /bin/sh -c 'ufw allow 22/tcp; rm -f /etc/systemd/system/ssh.socket.d/override.conf; systemctl daemon-reload; systemctl restart ssh.socket'

Cancel am with sudo systemctl stop ssh-rescue.timer once you confirm say the onion route still dey work.

Next, stop sshd from listening on the public address. Ubuntu 24.04 dey activate ssh through a socket unit, so ListenAddress inside sshd_config no get effect: ssh.socket dey control the listening socket, no be sshd. Check which case apply to you.

systemctl is-enabled ssh.socket

If e print enabled, run sudo systemctl edit ssh.socket and add this.

[Socket]
ListenStream=
ListenStream=127.0.0.1:22

The empty ListenStream= clears the value wey the packaged unit inherit. If you leave that line out, you go add second listener while the public one still dey active. Na this be the commonest way this step fit fail without showing error.

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
ss -tlnp | grep ':22'

ss suppose show 127.0.0.1:22 and nothing for 0.0.0.0:22. If ssh.socket was disabled, put ListenAddress 127.0.0.1 inside /etc/ssh/sshd_config.d/10-onion.conf, run sudo systemctl restart ssh, then check am with the same ss line. That output na the proof for either case.

Then handle the firewall, wey na normal ufw rule management for VPS. Run sudo ufw status numbered first, then delete any SSH rule wey e list.

sudo ufw status numbered
sudo ufw delete allow OpenSSH
sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw status verbose

Allow outgoing traffic. Tor dey connect out to relays for ports like 443 and 9001, so outbound default-deny policy go stop tor from bootstrapping and remove the only way wey remain to enter at the same time. Most providers still get separate network firewall for control panel. Close 22 there too, otherwise the port go remain reachable no matter wetin ufw report.

If Docker dey run for this box, check the ports wey e publish before you mark the job complete. Docker dey write e own rules inside the same tables and publish container ports direct past ufw, so ufw deny policy no cover the full picture.

Reboot before you trust am

systemctl is-enabled tor@default
sudo reboot

If the first command no report the service as enabled, run sudo systemctl enable tor@default before you reboot. Wait two minutes, then ssh myvps. Tor need bootstrap after boot, so the onion address go start answer some time after the machine itself don come up.

If e no ever come back, open the console and read sudo journalctl -u tor@default -b. Torrc syntax error or directory permission problem go show for there. You fit also check torrc edit before you apply am.

sudo -u debian-tor tor --verify-config

Wetyn e cost compared with a WireGuard tunnel

Compared with WireGuard VPN for your own VPS, onion service slow pass and e no dey predictable well. Be honest with yourself about the trade-off before you commit to am.

Latency. Client circuit get three relays, and service side add another three, so your keystrokes dey cross roughly six machines wey dem pick at random around the world. Interactive typing get visible delay, and file copies slow. WireGuard add one hop. Measure your own case with time ssh myvps 'echo ok', because the figure depend on which circuit tor build by chance, and e change when tor build another one.

A userspace daemon for the critical path. WireGuard dey for the kernel and e come up together with the network. Tor na process wey must start, bootstrap, and reach guard relay before anything go work. When e fail, na provider console you go use.

Clock accuracy. Onion service descriptors dey publish against time periods, so badly wrong clock fit break address lookup without any clear message anywhere. timedatectl suppose report System clock synchronized: yes.

Wetyn you get back na exposure wey no longer depend on firewall rule being correct. No port dey to scan and no banner dey to grab, and the address na public key itself, so the endpoint prove its identity before SSH even start.

The practical answer usually na both. Run WireGuard as the daily path, and keep the onion service as the route wey still dey work when WireGuard config wrong. That one leave one UDP port open instead of public SSH port. None of this replace hardening sshd itself: key-only authentication and non-root login still matter, because onion service protect the network path and nothing beyond am.

Wey errors fit happen and the errors wey you go see

Tor no dey pass Bootstrapped 0%. Outbound traffic dey blocked, or the clock time too far from correct time. Check the outgoing policy with sudo ufw status verbose, then run timedatectl.

systemctl status tor dey talk say active (exited). This one normal for Debian and Ubuntu. Read tor@default instead.

E no fit find the descriptor. Tor dey return SOCKS extended error F0, "Onion Service Descriptor Can Not be Found". E fit be say dem never publish the descriptor yet; after reload, e fit take small time. Or tor no dey run for the server.

F4, "Onion Service Missing Client Authorization". The client no get matching .auth_private wey tor fit use. Check say ClientOnionAuthDir dey inside torrc, say the directory mode na 0700, say the filename end with .auth_private, and say debian-tor fit read am.

F5, "Onion Service Wrong Client Authorization". The private key no match the .auth file for the server. If extra = dey for the end, or stray newline dey inside the base32 string, this error fit happen.

nc: invalid option -- 'X'. Na GNU netcat dey installed instead of the OpenBSD one. Run sudo apt install -y netcat-openbsd.

Could not resolve hostname. ssh try ordinary DNS, but DNS no get answer for .onion, so ProxyCommand never run. The Host pattern for ~/.ssh/config no match the name wey you type.

Permission denied (publickey). The tunnel work and tor don finish im own part. Treat this as ordinary permission denied publickey problem and leave tor out of the matter.

FAQ

Onion service really mean say no open ports dey on my VPS?

Yes, once sshd bind to 127.0.0.1 and firewall dey drop inbound traffic. Tor dey make outbound TCP connection go one relay, and your session dey pass through am come back. So nothing for the box dey accept connection for the public address. Prove am with ss -tlnp for the server and port scan from another place. No forget the provider own network firewall for the control panel. Na separate control from ufw, and you need close am too.

.onion address alone enough security for SSH?

No. The address get 56 characters and person no fit guess or enumerate am from the directory system. So e dey behave like secret, but shell history and config files fit leak am. Add v3 client authorisation. With am, service descriptor dey encrypted to your client key. So person wey only get the address go receive extended error F4 and e no go reach sshd at all.

Wetin go happen if tor no start after reboot?

You go lose SSH access completely, because onion address na the only way to enter then. Na why you need test the provider console before you close port 22. Tor still need time to bootstrap after boot, so the address go answer later than the machine respond to ping. If e never answer, log in through the console and read sudo journalctl -u tor@default -b. Na there torrc syntax error or permission problem for /var/lib/tor/ssh go show.

SSH over Tor slow pass WireGuard?

Yes, e slow by plenty. Connection to onion service dey cross about six relays wey dem choose randomly, while WireGuard na one encrypted hop direct to your server. Typing go feel laggy and transfers go slow. Common setup na to use WireGuard for daily work, while you keep onion service as emergency route wey fit still work if broken VPN config spoil.