Most homelab setups sit behind a router with no public IP, or behind CGNAT, or on a network you don’t control. The classic solution is port forwarding, dynamic DNS, a VPN, or a reverse tunnel. Each comes with its own headaches: firewall rules, DNS propagation, keeping a tunnel alive, trusting a third-party relay.
There’s a simpler option that solves all of these at once: Tor hidden
services (also known as “onion services”). You run the Tor daemon on both
ends, and it handles the routing. No ports to open, no NAT to configure, no
public IP needed. Your server gets a .onion address that only you can reach
and as a bonus, the entire connection is encrypted, anonymized and routed
through the Tor network.
The focus of this guide is not the privacy features of Tor, though you get those for free. The focus is practical: how to reliably SSH into a machine from anywhere in the world, with zero network configuration on the server side.
Both machines in this guide run Debian. The steps should be similar for Ubuntu and other Debian-based distributions. With minimal changes, you can also run this on macOS.
Part 1: Server setup
The server is the machine you want to SSH into, be it your homelab, a NAS server, a Raspberry Pi or a box under your desk at the office.
1.1. Add the Tor apt repository
The Tor project maintains its own Debian repository with up-to-date packages. Don’t use the version in Debian’s default repos as it is often outdated.
First, get the signing key:
$ wget https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc
You should check if the key has the correct fingerprint. It should match the
filename above (ending with 886D DD89). You can do it like this:
$ gpg A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc
gpg: WARNING: no command supplied. Trying to guess what you mean ...
pub rsa2048 2009-09-04 [SC] [expires: 2028-08-29]
A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89
uid deb.torproject.org archive signing key
sub rsa2048 2009-09-04 [S] [expires: 2027-11-01]
If you are confident with the output, dearmor the key and put it in the right place for apt:
$ sudo gpg --dearmor \
--output /usr/share/keyrings/deb.torproject.org-keyring.gpg \
A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc
Then add the repository. I’m using Debian Trixie, so we can use the new sources
syntax. Change “trixie” to your distro’s codename, that you can find using
lsb_release -cs. Add the following to /etc/apt/sources.list.d/tor.sources:
Types: deb deb-src
URIs: https://deb.torproject.org/torproject.org/
Suites: trixie
Components: main
Signed-By: /usr/share/keyrings/deb.torproject.org-keyring.gpg
If you are still stuck with the old syntax, create a tor.list instead:
deb [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] https://deb.torproject.org/torproject.org trixie main
1.2. Install Tor
I usually advise using --no-install-recommends for a minimal footprint and
cleaner system. You can always install more stuff later when you really need:
$ sudo apt update
$ sudo apt install tor --no-install-recommends
1.3. Create the hidden service directory
Tor needs a directory to store the hidden service keys and hostname. Create it
under /var/lib/tor/ and give ownership to the debian-tor system user. The
name ssh_proxy can be anything you want. That’s your hidden service internal
identifier:
$ sudo mkdir -p /var/lib/tor/ssh_proxy
$ sudo chown -R debian-tor: /var/lib/tor/ssh_proxy
$ sudo chmod 700 /var/lib/tor/ssh_proxy
1.4. Configure the hidden service
Edit /etc/tor/torrc and add/uncomment the following:
SocksPort 9050
DataDirectory /var/lib/tor
HiddenServiceDir /var/lib/tor/ssh_proxy/
HiddenServicePort 22 127.0.0.1:22
This tells Tor to expose port 22 (SSH) on the local machine as a hidden
service. The HiddenServicePort directive maps the onion service’s port 22 to
127.0.0.1:22. Exposed on localhost only, no external exposure.
1.5. Start Tor and get the onion address
Restart the Tor service to generate the hidden service keys:
$ sudo systemctl restart tor
Tor creates several files inside /var/lib/tor/ssh_proxy/:
hostname: your.onionaddresshs_ed25519_public_key: the service’s public keyhs_ed25519_secret_key: the service’s secret key
Grab the onion address:
$ sudo cat /var/lib/tor/ssh_proxy/hostname
Save this value, you’ll need it for the client configuration next.
At this point, the hidden service is running and anyone who knows the onion address can reach it. Since we want to restrict access to authorized clients only, we’ll set up client authentication next.
1.6. Generate a client key pair
Tor v3 onion services use x25519 key pairs for client authentication. The python-snippits repository has a handy script for generating them:
$ wget https://raw.githubusercontent.com/pastly/python-snippits/master/src/tor/x25519-gen.py
# You may need to install `pynacl` to run it
$ python3 x25519-gen.py
public: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
private: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
This outputs a public key and a private key. You’ll need both. The public key goes on the server, the private key goes on the client.
1.7. Authorize the client
Create a file in the authorized_clients directory with the public key. Each
client needs a file there with the correct public key. For example
/var/lib/tor/ssh_proxy/authorized_clients/client.auth with the
following content:
descriptor:x25519:$PUBLIC_KEY
Replace $PUBLIC_KEY with the actual public key from the previous step. The
file name (client.auth) can be anything, use something descriptive like
laptop.auth or work.auth if you have multiple clients.
Don’t forget to adjust permissions and ownership:
$ sudo chown debian-tor: /var/lib/tor/ssh_proxy/authorized_clients/client.auth
$ sudo chmod 640 /var/lib/tor/ssh_proxy/authorized_clients/client.auth
1.8. Restart Tor
$ sudo systemctl restart tor
From this point on, only clients holding the matching private key can connect to the hidden service. Anyone else attempting to reach the onion address will get nothing.
You now have two pieces of information to take to the client:
- The
.onionaddress fromhostname - The private key from step 1.6
Part 2: Client setup
The client is the machine you SSH from, such as your laptop, your phone, a workstation at a coffee shop, etc. This side is simpler as you won’t be running any hidden service, only the authentication details are required.
2.1. Add the Tor apt repository
Follow the same steps 1.1 and 1.2 described above for the installation on the server to get Tor installed on the client side.
2.2. Configure the Tor client
Edit /etc/tor/torrc to add or uncomment the following:
SocksPort 9050
ClientOnionAuthDir /var/lib/tor/onion_auth
Create the auth directory, which will store the private keys to authenticate against different onion services:
$ sudo mkdir -p /var/lib/tor/onion_auth
$ sudo chown -R debian-tor: /var/lib/tor/onion_auth
$ sudo chmod 700 /var/lib/tor/onion_auth
2.3. Add the private key
Create an .auth_private file with the private key from step 1.6. The filename
here matters! You’ll use this name to identify the server you want to connect to.
Use a descriptive nickname: homelab.auth_private, raspberry.auth_private,
office.auth_private or something else that makes sense for you. If you connect
to multiple onion services, you’ll have one file per server in this directory.
In this example, let’s create /var/lib/tor/onion_auth/homelab.auth_private with
the following content:
$ONION_ADDRESS:descriptor:x25519:$PRIVATE_KEY
Replace $ONION_ADDRESS with the hostname you collected on step 1.5 above and
the $PRIVATE_KEY from step 1.6. ATTENTION: don’t include the .onion
part of the hostname, just the first part with the random address.
Finally, fix the ownership and permissions:
$ sudo chown debian-tor: /var/lib/tor/onion_auth/homelab.auth_private
$ sudo chmod 640 /var/lib/tor/onion_auth/homelab.auth_private
2.4. Restart Tor
$ sudo systemctl restart tor
2.5. Configure SSH
At this point, you should already be able to connect to the remote server
using nc -x localhost:9050 $ONION_ADDRESS.onion 22, but that would give
you a raw TCP connection, which is not what we want.
In order to establish a valid SSH session and do the whole thing more easily,
let’s add an entry to ~/.ssh/config:
Host homelab
Hostname $ONION_ADDRESS.onion
User $USERNAME
Port 22
CheckHostIP no
ProxyCommand /usr/bin/nc -x localhost:9050 %h %p
Replace $ONION_ADDRESS with your full .onion address and $USERNAME with
your SSH user on the server. The homelab here matches the file we created
homelab.auth_private, you should change it to whatever you used on that
step.
CheckHostIP no is needed because .onion addresses don’t resolve to stable
IPs, so the check would fail every time.
If you are authenticating with password, you may want to add this as well:
PreferredAuthentications password
PubkeyAuthentication no
The ProxyCommand routes the SSH connection through the local Tor SOCKS proxy
on port 9050, which is the port Tor is listening to.
The -x flag tells netcat to use a SOCKS proxy. This requires the
netcat-openbsd package. Debian systems sometimes ship with
netcat-traditional instead, which doesn’t support -x. Check which one you
have:
$ readlink -f "$(which nc)"
If it points to nc.traditional, install the OpenBSD variant:
$ sudo apt install netcat-openbsd
2.6. Connect
With the SSH configuration in place, connecting to the server is as easy as running the following:
$ ssh homelab
That’s it. The first connection will take a few seconds while Tor establishes the circuit, but after that you have a regular SSH session, just routed through the Tor network.
Why this works
Tor onion services solve several problems at once:
- No port forwarding: the server makes outbound connections to the Tor network, not inbound. Your router’s firewall doesn’t matter.
- No public IP needed: works behind CGNAT, double NAT, corporate firewalls, hotel Wi-Fi, anything.
- No dynamic DNS: the
.onionaddress is derived from the service’s cryptographic key, not from an IP. It never changes. - End-to-end encryption: the connection is encrypted between client and server, independent of SSH’s own encryption. Two layers.
- Client authentication: with x25519 keys, only authorized clients can even reach the service. Unauthorized attempts get no response at all, the server is invisible.
The trade-off is latency. Tor routes traffic through multiple relays, so expect
200–800ms round-trip times. For interactive SSH sessions, this is noticeable
but workable. For scp or rsync, it’s slow. You’ll want a different
solution for bulk file transfers.
But for command-line access to a homelab from a random coffee shop? It’s hard to beat this one.