Using TOR like a VPN to make SSH available

Back in the day, the machines I used to maintain either had public IP addresses or were local to my home network; that has changed. I still want to regularly log in to devices "on the road" by means of SSH.

The approaches I considered were:

Warning

Even though this may sound secure due to the use of TOR hidden services, it does not attempt to be. I would have been perfectly OK with just opening the SSH port to the network (relying on SSH to be secure and not needing any more authentication from the client) and announcing that address, and that's the only security level I expect. Do not follow the steps shown here without understanding what they mean and the implications on your device's security.

Server setup

On Debian, making SSH available via TOR is really straightforward:

After restarting SSH and TOR, the .onion name for that host can be read from /var/lib/tor/hidden_service/hostnme; I'll be using abc1234567890.onion as an example name here.

Note

It's not a fully hidden service

The service run here should not be mistaken for having any privacy properties in the sense of hiding the server. The service uses the same host key on TOR and in the LAN, so everyone who knows your host key will know who runs this servicE. toR is used only for discovery here.

TOR can probably be configured for the hidden service to only use one hop, this could increase performance and decrease the load on the TOR network; I did not get around to trying this yet.

Client setup I

For a first test, I set up my SSH client manually in ~/.ssh/config:

Host myhost.example.com
    ProxyCommand nc -X 5 -x localhost:9050 abc1234567890.onion 22

After that, ssh myhost.example.com always went through TOR.

For some setups, that may be sufficient; I chose to take it a little further.

DNS setup

As I prefer not to have explicit configuration about every server on every client machine (as I would need to with the above), I decided to publish the .onion names of the hosts; the most logical choice seems to be in DNS.

To avoid confusing other services, I went for a mechanism that looks a bit like DNS-SD (but is not, as .onion addresses are not "real" host names, and SSH does not use DNS-SD anyway). In my zone file for example.com, it looks like this:

_ssh._onion.myhost.example.com IN TXT abc1234567890.onion

Those I publish with knot DNS, but any DNS server should be able to take those as static records.

Client setup II

I now only keep a single stanza in my SSH config entry:

Host *
    ProxyCommand ~/.config/maybe-tunnel %h %p

This makes all connections go through an (executable) helper script of the following content:

#!/bin/sh
set -e

if getent hosts $1 >/dev/null
then
        exec nc $1 $2
else
        torhost="`dig +short TXT _ssh._onion."$1" | grep \\.onion | sed 's/"//g'`"

        if [ x = x"$torhost" ]
        then
                echo "Host $1 could not be resolved, and has no _ssh._onion TXT record either." >&2
                exit 1
        fi
        exec nc -X 5 -x localhost:9050 "$torhost" $2
fi

This script looks up the configured host name. If it resolves, it uses nc to open a direct connection, emulating what SSH does by default. (If SSH accepted dynamic configuration, I would just not set a ProxyCommand at all in this case). This covers both the case of "regular" servers that have public IPs, and also makes local connections work, because as long as I am inside the example.com LAN and the other machine is too, the internal dnsmasq server will answer the request with the local address.

If the address does not resolve, the script looks for an _ssh._onion record that would tell the address of a hidden service announced as per the above. If one is found, a connection is established using the local TOR instance (running on port 9050 by default).

Note

SSH host keys

As I use this setup right now, I either establish the hosts' identity once-per-client when I first use them in the LAN (so their public host keys get stored in my user's known_hosts file under their .example.com name), and/or I store their public keys in the known_hosts file I deploy to my boxes.

If one uses DNSSEC, it should be feasible to announce SSHFP records for myhost.example.com (even though there's no A/AAAA record for that host) and trust those.

Outlook

This does not interact well when both client and server are in the same public WiFi and could discover each other locally (they'd still go via TOR).

It would be interesting to extend this to proper DNS-SD discovery with multicast -- essentially, once a host has a known key (from TOFU or SSHFP records), whatever has no A/AAAA record would be up for local discovery, falling back to TOR based discovery.

TOR was what worked easy and fast, but its onion routing properties are not really being used here. Other schemes that "just" provide a TCP turnover point might be more convenient here. Obviously, if anything were to be usable without per-client customization, that' be great, but as SSH does not really support virtual hosting, that might be tricky.

Final words

This is an experimental setup I have just established, and am rolling out to my mobile devices; I have no practical experience with it yet.

I'd appreciate any comments on it to (preferably by mailto:chrysn@fsfe.org). I will post updates here if something significant changes.

Tags:blog-chrysn