SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-29

SSH Through Tor Onion Service Without Open Ports

Put sshd behind a Tor onion service and close every inbound port. See v3 client authorisation, recovery console steps, and the order wey prevents lockout.

Wetin SSH over Tor onion service change

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

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

The cost na say tor dey inside the path of every admin session. Na userspace daemon wey need 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 recovery path 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. Log in with am. If you no know the root password, reset the root password from the panel first, then confirm say e work. Console wey you never test no be recovery path.

The order below matter. You go confirm each step before the next one run, and port 22 go remain open until the onion route 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. 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 inside im repository, but that build often dey behind. 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 use 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 stuck below there, tor no fit reach network. The common causes na outbound firewall rule or clock wey wrong well-well.

The unit name na trap. systemctl status tor dey 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 pull 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 dey reach the instance, so sudo systemctl reload tor go work as you expect.

Defaine 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 to accept virtual port 22 for the onion address and connect am to 127.0.0.1:22 for the box. Tor dey reach sshd through loopback, and na why sshd fit later stop to listen for the public address. Point that second line to a web server for 127.0.0.1:80 instead, and the same two directives publish a site for an onion address, wey be useful second service to run after tor don install.

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 anywhere for this setup.

Make tor create /var/lib/tor/ssh/ by itself. If you make am by hand with wrong owner or mode wey loose pass 0700, tor go refuse to use am, and 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, because if you lose am, you go get new address and 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, and 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 one 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 pass the onion name to tor as a name, so tor go resolve am inside the network. This one must be the OpenBSD netcat. GNU netcat no get -X option and e stop with nc: invalid option -- 'X'.

ssh myvps

The first connection go 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 move. Authentication no move.

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 be now, anybody wey learn the address fit reach your SSH banner and start guessing. Directory system no fit enumerate onion addresses, so the address dey behave like secret, but e fit leak through normal ways: shell history and config files wey person commit to a git repository. Client authorisation close this gap. The service publish its descriptor encrypted to a client key, so person wey get the address but no key no fit even locate the service.

Generate an 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 dey use base64pem -d, but stock Ubuntu install no get am. The command then stop with base64pem: command not found. GNU base64 -d fit 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 dey get read. Save am as laptop.auth.txt. If you save am with another filename, 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 the debian-tor user and e no fit read files inside your home directory, so keep the directory for somewhere 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 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 the .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

First set safety net. This one command go undo both changes 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 for sshd_config no get effect: ssh.socket dey control the listening socket, no be sshd. Check which situation 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= go clear the value wey the packaged unit inherit. If you leave that line out, you go add another listener while the public one still dey active. Na this be the commonest way this step fit fail without showing any 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 dey disabled, put ListenAddress 127.0.0.1 for /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 firewall. This one na normal ufw rule management for VPS. Run sudo ufw status numbered first and 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

Leave outgoing traffic allowed. Tor dey connect out to relays for ports like 443 and 9001, so outbound default-deny policy go stop tor from bootstrapping and remove your only remaining way to enter at the same time. Most providers still get separate network firewall for the 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 call the job complete. Docker dey write e own rules into the same tables and publish container ports directly past ufw, so ufw deny policy no be the full picture.

Reboot before you trust am

systemctl is-enabled tor@default
sudo reboot

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

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

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

What this costs next to a WireGuard tunnel

Compared with WireGuard VPN for your own VPS, onion service dey slower 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 go cross roughly six machines wey dem pick at random around the world. You go notice delay when you dey type, and file copies go slow. WireGuard add one hop. Measure your own case with time ssh myvps 'echo ok', because the number depend on the circuit wey tor build that time, and e go change when tor build another one.

A userspace daemon in the critical path. WireGuard dey for the kernel and e dey come up together with the network. Tor na process wey must start, bootstrap, and reach a 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 clock wey dey seriously wrong fit break address lookup without any clear message anywhere. timedatectl suppose report System clock synchronized: yes.

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

The practical answer usually na both. Run WireGuard as your daily path, and keep onion service as the route wey still go 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.

Failure modes and the errors wey you go see

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

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

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

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. Extra = for the end or stray newline inside the base32 string fit cause this.

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

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

Permission denied (publickey). The tunnel work and tor don finish. Treat am as ordinary permission denied publickey problem and no involve tor for the matter.

FAQ

Onion service really mean say your VPS no get any open port?

Yes, once sshd dey bound to 127.0.0.1 and firewall dey drop inbound traffic. Tor go make outbound TCP connection to a relay, and your session go pass back through am. So nothing for the box dey accept connection on 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. E separate from ufw, and you must close am too.

.onion address alone get enough security for SSH?

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

Wetin go happen if tor no start after reboot?

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

SSH through Tor slow pass WireGuard?

Yes, e slow well-well. Connection to an onion service dey pass through 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 the onion service as emergency route wey still work if broken VPN config spoil access.