Unix Domain Socket Forwarding with OpenSSH
OpenSSH is well-known for its ability to forward TCP ports from a local host to a remote host and vice versa. Typical use cases include:
Access an otherwise unreachable server via a bastion host.
Access the loopback interface of a remote host.
Expose a local network service to a remote host.
Recently, I had an interesting variant of the third use case: I wanted to expose a local network service — in this case, an inherently insecure RFB server — via a remote host on condition that only a select user could connect to the exposed service. That is, I wanted to control access to the remote forwarding. It turned out that Unix domain socket forwardings are better suited than TCP port forwardings in this case.
In the remainder of this post, I explain why it is difficult to control access to a TCP port forwarding, and how you can control access using a Unix domain socket forwarding instead. Moreover, I describe how to restrict forwardings to select TCP ports respectively socket files.
TCP port forwarding #
The following command forwards port 8080 on a remote host to port 80 on the local host:1
$ ssh -nNT \
-R 8080:localhost:80 \
-o "ExitOnForwardFailure yes" \
remote_host
Of course, you can restrict port forwardings by means of the sshd_config(5)
or an authorized_keys
file.2
For example, the following settings restrict the user Alex to listen to — and thus forward — remote port 8080 only:
Match User alex
AllowTcpForwarding remote
PermitOpen none
PermitListen 8080
PermitTTY no
ForceCommand /bin/echo 'ssh command forced by sshd_config(5)'
However, as the man page clearly states, users with access to a shell can generally bypass this restriction:
AllowTcpForwarding
Specifies whether TCP forwarding is permitted. The available options are yes (the default) or all to allow TCP forwarding, no to prevent all TCP forwarding, local to allow local (from the perspective of ssh(1)) forwarding only or remote to allow remote forwarding only. Note that disabling TCP forwarding does not improve security unless users are also denied shell access, as they can always install their own forwarders.
For example, the following command uses the infamous netcat utility — in this case OpenBSD’s widespread reimplementation, nc(1)
— to query the exposed HTTP service at port 8080:
$ printf "GET / HTTP/1.0\r\n\r\n" \
| ssh -T remote_host nc localhost 8080
As far as I know, you have two practical options to control access to forwardings:
Add user-specific rules to your firewall of choice, if supported. For example, the owner module of
iptables(8)
enables you to match the user ID and the group ID of a local packet creator.Use Unix domain sockets instead of TCP ports, and protect the special socket files just like regular files — i.e., set the file owner, group, and mode using
chown(8)
andchmod(1)
.
Unix domain socket forwarding #
OpenSSH supports Unix domain socket forwarding out of the box.3 Simply specify a file name instead of a TCP port. For example, the following command creates, binds to, and forwards a socket on a remote host to port 80 on the local host:
$ ssh -nNT \
-R /var/run/http.sock:localhost:80 \
-o "ExitOnForwardFailure yes" \
remote_host
With this, you can query the exposed HTTP service again using nc(1)
:
$ printf "GET / HTTP/1.0\r\n\r\n" \
| ssh -T remote_host nc -U /var/run/http.sock
There are two caveats, though:
The file name of the socket must contain a forward slash. Otherwise,
ssh(1)
misinterprets the socket name as a TCP port.The file name of the socket should not rely on tilde expansion. That is, use
/home/alex/http.sock
instead of~/http.sock
or~alex/http.sock
.You must not re-bind an existing socket by default.4 Set
StreamLocalBindUnlink yes
in thesshd_config(5)
to allow this.There are no
sshd_config(5)
options equivalent toPermitOpen
andPermitListen
for Unix domain sockets. That is, you cannot restrict the file name of the socket.
However, if you really want to restrict the socket’s file name, then we can build upon the netcat trick from above.
Restricting the socket’s file name #
This time we’ll use socat(1)
, another successor of netcat.5
The following command effectively establishes the same forwarding as the previous one: it creates, binds to, and forwards a socket on a remote host to port 80 on the local host.
$ socat \
EXEC:'ssh -T remote_host socat "UNIX-LISTEN:/var/run/http.sock,fork,unlink-early" STDIO' \
TCP4:127.0.0.1:80,fork
Essentially, the command builds the following bidirectional stream:
socat(1)
pipes the remote socket to standard input/output on the remote host.ssh(1)
pipes this input/output from the remote host to the local host.socat(1)
pipes the input/output fromssh(1)
to port 80 on the local host.
With this, we can restrict the socket’s name by means of the sshd_config
or an authorized_keys
file as follows:
Match User alex
DisableForwarding yes
PermitTTY no
ForceCommand /usr/local/bin/socat UNIX-LISTEN:/var/run/http.sock,fork,unlink-early STDIO
With these settings, the previous commands comes down to the following, where forced-command
is an optional no-op reminder:
$ socat \
EXEC:'ssh -T remote_host forced-command' \
TCP4:127.0.0.1:80,fork
Finally, a client may connect to this socket as follows — regardless of how we created the socket:
$ ssh -nNT \
-L 3000:/var/run/http.sock \
-o "ExitOnForwardFailure yes" \
remote_host
$ curl http://localhost:3000/
Unfortunately, ssh(1)
’s ExitOnForwardFailure
option does not catch missing permissions to access the socket file.
Thus, if the final curl(1)
command fails and you cannot actually use the forwarding, please check the group and the mode of the socket file created by socat(1)
on the remote host.
You can set the group and mode using the corresponding UNIX-LISTEN
options.
Conclusion #
OpenSSH is able to forward TCP ports and Unix domain sockets. The server can be configured to restrict who may open respectively listen to which port. However, users with access to a shell can generally bypass this restriction. In particular, any user with access to a shell can open any forwarded port. Thus, you might want to use Unix domain sockets instead of TCP ports for remote forwardings. This way, you can control access to the special socket file, and thus to the forwarded network service. You can restrict the socket’s file name by forcing a special command instead of using the built-in forwarding.
I use the descriptive
localhost
hostname instead of actual addresses on the loopback interface such as127.0.0.1
or::1
throughout this blog post. However, the truly paranoid hacker might want to use the latter to save the hostname resolution.↩︎OpenSSH is developed as a part of OpenBSD. That’s why I’m referring to OpenBSD’s man pages here. Besides, I’m referring to the man pages at the time of writing.↩︎
Notably, the OpenSSH version shipped with Windows 10 does not support Unix domain socket forwarding, see here.↩︎
There is no system call to re-bind an existing Unix domain socket. Instead, you have to
unlink(2)
— and thereby remove — the socket file before you canbind(2)
andlisten(2)
again.↩︎In my opinion, the following two commands should be equivalent:
$ socat UNIX-LISTEN:foo.sock,fork TCP4:127.0.0.1:8080,fork $ mkfifo backpipe $ nc -lkU foo.sock 0<backpipe \ | nc 127.0.0.1 8080 1>backpipe
However, in practice, the second command did not work reliably on OpenBSD 6.6 and Arch Linux in June 2020. I suspect, it’s got something to do with an early EOF or standard output buffering. Please drop me a mail if you can help me out.↩︎