Unix Domain Socket Forwarding with OpenSSH

Posted on

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:

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:

  1. 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.

  2. 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) and chmod(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:

  1. The file name of the socket must contain a forward slash. Otherwise, ssh(1) misinterprets the socket name as a TCP port.

  2. 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.

  3. You must not re-bind an existing socket by default.4 Set StreamLocalBindUnlink yes in the sshd_config(5) to allow this.

  4. There are no sshd_config(5) options equivalent to PermitOpen and PermitListen 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:

  1. socat(1) pipes the remote socket to standard input/output on the remote host.

  2. ssh(1) pipes this input/output from the remote host to the local host.

  3. socat(1) pipes the input/output from ssh(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.


  1. I use the descriptive localhost hostname instead of actual addresses on the loopback interface such as 127.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.↩︎

  2. 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.↩︎

  3. Notably, the OpenSSH version shipped with Windows 10 does not support Unix domain socket forwarding, see here.↩︎

  4. 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 can bind(2) and listen(2) again.↩︎

  5. 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.↩︎