1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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](https://ipset.netfilter.org/iptables-extensions.man.html#lbBP) of [`iptables(8)`](https://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.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://learn.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://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.
|