summaryrefslogtreecommitdiff
path: root/posts/unix-domain-socket-forwarding-with-openssh.md
diff options
context:
space:
mode:
Diffstat (limited to 'posts/unix-domain-socket-forwarding-with-openssh.md')
-rw-r--r--posts/unix-domain-socket-forwarding-with-openssh.md173
1 files changed, 173 insertions, 0 deletions
diff --git a/posts/unix-domain-socket-forwarding-with-openssh.md b/posts/unix-domain-socket-forwarding-with-openssh.md
new file mode 100644
index 0000000..295b0c2
--- /dev/null
+++ b/posts/unix-domain-socket-forwarding-with-openssh.md
@@ -0,0 +1,173 @@
+---
+title: Unix Domain Socket Forwarding with OpenSSH
+description: How to control access to OpenSSH forwardings using Unix domain sockets instead of TCP ports.
+published: 2020-07-06
+---
+
+[OpenSSH](https://www.openssh.com/) 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](https://www.iana.org/assignments/rfb/rfb.xml) 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:[^localhost]
+
+ $ 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)`](https://man.openbsd.org/OpenBSD-6.6/sshd_config) or an [`authorized_keys`](https://man.openbsd.org/OpenBSD-6.6/sshd#AUTHORIZED_KEYS_FILE_FORMAT) file.[^manuals]
+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](https://man.openbsd.org/OpenBSD-6.6/sshd_config#AllowTcpForwarding), 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](https://nc110.sourceforge.io/) utility --- in this case OpenBSD's widespread reimplementation, [`nc(1)`](https://man.openbsd.org/OpenBSD-6.6/nc) --- 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](http://ipset.netfilter.org/iptables-extensions.man.html#lbBP) of [`iptables(8)`](http://ipset.netfilter.org/iptables.man.html) enables you to match the user ID and the group ID of a local packet creator.
+
+1. Use [Unix domain sockets](https://en.wikipedia.org/w/index.php?title=Unix_domain_socket&oldid=949050080) 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)`](https://man.openbsd.org/OpenBSD-6.6/chown) and [`chmod(1)`](https://man.openbsd.org/OpenBSD-6.6/chmod).
+
+## Unix domain socket forwarding
+
+OpenSSH supports Unix domain socket forwarding out of the box.[^windows]
+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)`](https://man.openbsd.org/OpenBSD-6.6/nc):
+
+ $ 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)`](https://man.openbsd.org/OpenBSD-6.6/ssh) misinterprets the socket name as a TCP port.
+
+1. The file name of the socket should not rely on [tilde expansion](https://man.openbsd.org/OpenBSD-6.6/ksh#Tilde_expansion).
+That is, use `/home/alex/http.sock` instead of `~/http.sock` or `~alex/http.sock`.
+
+1. You must not re-bind an existing socket by default.[^unlink] Set `StreamLocalBindUnlink yes` in the [`sshd_config(5)`](https://man.openbsd.org/OpenBSD-6.6/sshd_config) to allow this.
+
+1. There are no [`sshd_config(5)`](https://man.openbsd.org/OpenBSD-6.6/sshd_config) 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)`](http://www.dest-unreach.org/socat/doc/socat.html), another successor of netcat.[^backpipe]
+
+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)`](http://www.dest-unreach.org/socat/doc/socat.html) pipes the remote socket to standard input/output on the remote host.
+
+1. [`ssh(1)`](https://man.openbsd.org/OpenBSD-6.6/ssh) pipes this input/output from the remote host to the local host.
+
+1. [`socat(1)`](http://www.dest-unreach.org/socat/doc/socat.html) pipes the input/output from [`ssh(1)`](https://man.openbsd.org/OpenBSD-6.6/ssh) to port 80 on the local host.
+
+With this, we can restrict the socket's name by means of the [`sshd_config`](https://man.openbsd.org/OpenBSD-6.6/sshd_config) or an [`authorized_keys`](https://man.openbsd.org/OpenBSD-6.6/sshd#AUTHORIZED_KEYS_FILE_FORMAT) 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)`](https://man.openbsd.org/OpenBSD-6.6/ssh)'s `ExitOnForwardFailure` option does not catch missing permissions to access the socket file.
+Thus, if the final [`curl(1)`](https://curl.haxx.se/docs/manpage.html) command fails and you cannot actually use the forwarding, please check the group and the mode of the socket file created by [`socat(1)`](http://www.dest-unreach.org/socat/doc/socat.html) 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.
+
+[^localhost]:
+ 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.
+
+[^manuals]:
+ OpenSSH is developed as a part of [OpenBSD](https://www.openbsd.org/).
+ 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.
+
+[^windows]:
+ Notably, the OpenSSH version shipped with Windows 10 does not support Unix domain socket forwarding, see [here](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_server_configuration).
+
+[^unlink]:
+ There is no system call to re-bind an existing Unix domain socket.
+ Instead, you have to [`unlink(2)`](https://man.openbsd.org/OpenBSD-6.6/unlink) --- and thereby remove --- the socket file before you can [`bind(2)`](https://man.openbsd.org/OpenBSD-6.6/bind) and [`listen(2)`](https://man.openbsd.org/OpenBSD-6.6/listen) again.
+
+[^backpipe]:
+ 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](https://www.openbsd.org/66.html) and [Arch Linux](https://www.archlinux.org/) 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.
Generated by cgit. See skreutz.com for my tech blog and contact information.