In my previous two posts I set up a login prompt on a bluetooth serial port and then switched to running SSH on it.

I explicitly did not set up an IP network over bluetooth as I want to minimize the number of configurations (e.g. IP address) and increase the chance of it working when needed.

E.g. firewall misconfiguration or Linux’s various “clever” network managers that tend to wipe out network interface configs would have more of a shared fate with the primary access method (SSH over normal network).

This post is about how to accomplish this more properly.

The problems now being solved are:

  • It wasn’t entirely reliable. The rfcomm tool is pretty buggy.

  • There was no authentication of the Bluetooth channel. Not as much a problem when doing SSH, but if there are passwords then there could be a man-in-the-middle attack.

  • The server side had to remain discoverable forever. So anyone who scans for nearby bluetooth devices would see your servers, and would be able to connect, possibly brute forcing passwords. Not as much of a problem if running SSH with password authentication turned off, but why broadcast the name of a server if you don’t have to?

So here we’ll instead explicitly pair with a PIN just once, and then turn off discoverability.

If you’re happy with no PIN and being discoverable all the time, then aside from the actual SSH connect and listen commands you can just run hciconfig hci0 piscan and then the SSH commands.

Step 0: Install my helpers on both server and client

git clone
cd bthelper
./ && ./configure && make && make install

1. On the server: get the MAC

This will be needed by the client to pair and to connect.

$ hcitool dev
        hci0     AA:BB:CC:DD:EE:FF

2. On the server: Start a PIN agent

By default PINs will be skipped (or just use a well-known like 0000), it seems.

At first I tried activating an agent inside bluetoothctl, but from what I can tell it’s only “reactive”, in that it’ll ask the user only if it’s needed. This means that if both sides use this agent then they will “just work” without bothering with a PIN.

So we need an agent that goes “no, really. You need a PIN”.

$ sudo apt install bluez-tools
$ sudo bt-agent

Let this run in the terminal during pairing, as this agent will make up a PIN and will print it to stdout, which you’ll need during pairing.

3. On the server: Turn off built-in agent and enable discoverability

# bluetoothctl
[bluetooth]# agent off
[bluetooth]# discoverable yes

This makes the server show up in scans, and is required (as far as I can tell) in order to pair.

Note that the device and its name are now visible to anyone within range.

4. On the client: pair

sudo bluetoothctl
[bluetooth]# scan on
[bluetooth]# pair AA:BB:CC:DD:EE:FF
[CHG] Device AA:BB:CC:DD:EE:FF Connected: yes
Request passkey
[agent] Enter passkey (number in 0-999999): <enter number from server's agent here>
[CHG] Device AA:BB:CC:DD:EE:FF Paired: yes

Now the two devices are paired and share a link key. You can inspect it in on both sides in /var/lib/bluetooth/<my mac address>/<peer mac address>/info as LinkKey.

When the link key is set up there’s no longer any need for the server to be discoverable.

Now you can turn scanning back off

[bluetooth]# scan off

5. On the server: turn discover mode back off, and agent back on

[bluetooth]# discoverable no
[bluetooth]# agent on

6. On the server: kill the bt-agent

Control-C will do it.

7. On the server: Set up SSH service on an RFCOMM channel

Here I randomly pick 3. It’s possible to choose channel 0, and then register it with the “UUID to channel name” lookup service that is SDP.

But here I want minimal complexity and dependency, so I’m skipping that.

# cat > /etc/systemd/system/bluetooth-ssh.service
Description=Bluetooth SSH

ExecStart=/usr/local/bin/bt-listener -c 3 -t

# systemctl enable bluetooth-ssh
# systemctl start bluetooth-ssh

This can also be done with rfcomm watch, but I found it to be very unstable and CPU-heavy (~35% CPU on raspberry pi when idling). It also needlessly allocates a /dev/rfcomm<N> device (requiring running as root). And it looks like rfcomm in bluez is deprecated anyway.

But it can also be done with socat exec:"/usr/local/bin/bt-listener -c 3" tcp:, but this doesn’t allow for concurrent connections, and requires waiting for RestartSec seconds before a new connection can be started after another.

8. On the client: Configure an SSH alias

$ cat >> ~/.ssh/config
Host servernamehere-console
     ProxyCommand /usr/local/bin/bt-connecter AA:BB:CC:DD:EE:FF 3

Now you should be able to ssh servernamehere-console.