summaryrefslogtreecommitdiff
path: root/_drafts/ssh-proxy.md
blob: 26a68c0cacc83846818115268da64bf97f9a6ebc (plain)
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
---
title: "SSH Proxy"
description: "How to establish a secure tunnel between two firewalled machines."
published: 2020-06-22
---

Consumer grade internet connections typically prevent inbound connections by means of network address translation (NAT) or firewalls.
While this protects consumers to some degree from the evil outside, it also hinders them from providing network services.
Luckily, people came up with several [NAT traversal techniques](https://en.wikipedia.org/w/index.php?title=NAT_traversal&oldid=950406393#Techniques) including the widespread [SOCKS](https://en.wikipedia.org/w/index.php?title=SOCKS&oldid=963014782) protocol and the martial [UDP hole punching](https://en.wikipedia.org/w/index.php?title=UDP_hole_punching&oldid=957144154).
However, I found that [OpenSSH](https://www.openssh.com/)'s little-known remote port forwarding feature enables a simple and yet secure alternative that I would like to share in this post.

We'll use the [OpenSSH](https://www.openssh.com/) client and server here because its a free, battle-tested, and portable implementation of the SSH protocol.
Chances are, that your operating system ships with OpenSSH built-in or packaged.
In fact, even [macOS](https://support.apple.com/guide/remote-desktop/about-systemsetup-apd95406b8d/mac) and [Windows 10](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_overview) include OpenSSH, activatable from the command-line.

Contents:

* TCP port forwarding. Anyone on the proxy may access the exposed service.
* End-to-end encrypted tunnel. Servers needs to run local SSH server.
* Unix domain socket forwarding. Protected socket instead of TCP port.

## TL;DR

Basically, you can ...

You've got three different options to share a local network service by means of a publically reachable OpenSSH proxy:

1. Create a remote port forwarding from the proxy to the server and 

Authentication and authorization aside, you can share a local network service in three steps:

1. Create a remote port forwarding from a publically reachable proxy to the firewalled server:

       $ ssh -nNTR 8080:localhost:80 proxy.example.com

1. Create a local port forwarding from the client to the proxy:

       $ ssh -nNTL 8080:localhost:8080 proxy.example.com

1. Access the exposed network service from the client:

       $ curl http://localhost:8080/

So far so good, but the real work is of course to setup the public key authentication and to restrict the keys' permissions.
Thus, the remainder of this blog post describes a typical use case in detail.

## Screen sharing example

Let's say, Alex wants to share his screen with Tyler.
Sure enough, Alex can run a local [RFB](https://www.iana.org/assignments/rfb/rfb.xml) server such as [TigerVNC](https://tigervnc.org/) or [Apple Remote Desktop](https://support.apple.com/remote-desktop) on port 5900.
But thanks to their internet provider's NAT, neither of the two may connect to the other directly.
Fortunately, Tyler has access to a publicly reachable OpenSSH server that they can use as a forward proxy as follows.

Note:
OpenSSH is developed as a part of [OpenBSD](https://www.openbsd.org/).
Thus, it comes with excellent [man pages](https://www.openssh.com/manual.html).
So please _read the fine manual_ to understand the various options that we'll use.
I cannot describe them better.

First, Alex starts an OpenSSH server on his machine.
By default, the SSH server should listen on port 22 of all local addresses and permit both, password and public key authentication, see [`sshd_config(5)`](https://man.openbsd.org/sshd_config).
This is sufficient for our setup.
However, the truly paranoid may safely bind the SSH server to the loopback interface only and forbid password authentication:

    ListenAddress 127.0.0.1
    ListenAddress ::1
    Port 22
    PasswordAuthentication no
    PubkeyAuthentication yes

Second, Alex prepares a [`known_hosts`](https://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) file for Tyler:

    $ cat /etc/ssh/ssh_host_*_key.pub \
      | sed -e 's/^/alex_workstation /' \
      | tee -a alex_public_host_keys
    alex_workstation ssh-dss AAAAB3N... root@alex.localdomain
    alex_workstation ssh-ed25519 AAAAC3N... root@alex.localdomain
    alex_workstation ssh-rsa AAAAB3N... root@alex.localdomain

Third, Alex generates an SSH key to authenticate at the proxy.
We'll generate a dedicated key here, so you can follow this guide without using your normal SSH key:

    $ ssh-keygen -f ~/.ssh/id_proxy

Fourth, Tyler imports Alex' public host keys on his machine:

    $ cat alex_public_host_keys >> ~/.ssh/known_hosts

Fifth, Tyler authorizes Alex to connect to the proxy.
That is, Tyler adds Alex' public user key to his personal [`authorized_keys`](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT) file on the proxy, applying the following restrictions to prevent Alex from misusing Tyler's account:

    restrict,command="echo 'ssh command restricted by authorized_keys'",port-forwarding,permitlisten="2222" ssh-rsa AAAAB3N...

The truly paranoid may wish to create a dedicated [`nologin(8)`](https://man.openbsd.org/nologin) user account on the proxy instead and apply the above restrictions by means of the [`sshd_config(5)`](https://man.openbsd.org/sshd_config):

    Match User port-forward-only
      DisableForwarding yes
      ForceCommand echo 'ssh command forced by sshd_config'
      PermitTTY no
      AllowTcpForwarding remote
      PermitListen 2222

Sixth, Tyler prepares a [`known_hosts`](https://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) file for Alex:

    $ cat /etc/ssh/ssh_host_*_key.pub \
      | sed -e 's/^/[proxy.example.com]:22 /' \
      | tee -a proxy_public_host_keys
    [proxy.example.com]:22 ssh-dss AAAAB3N... root@proxy.example.com
    [proxy.example.com]:22 ssh-ed25519 AAAAC3N... root@proxy.example.com
    [proxy.example.com]:22 ssh-rsa AAAAB3N... root@proxy.example.com

Seventh, Alex imports the public host keys of the proxy on his machine:

    $ cat proxy_public_host_keys >> ~/.ssh/known_hosts

Eighth, Alex authorizes Tyler to connect to his machine.
That is, Alex adds Tyler's public user key to his personal [`authorized_keys`](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT) file on his machine, applying the following restrictions to prevent Tyler from misusing Alex' account:

    restrict,command="echo 'ssh command restricted by authorized_keys'",port-forwarding,permitopen="5900" ssh-rsa AAAAB3N...

Again, the truly paranoid may wish to create a dedicated user account as described above.

Ninth, Alex connects to the proxy and forwards port 2222 to port 22 on his machine:

    $ ssh \
      -nNT \
      -R 2222:localhost:22 \
      -i ~/.ssh/id_proxy \
      -o "IdentitiesOnly yes" \
      -o "StrictHostKeyChecking yes" \
      -o "ExitOnForwardFailure yes" \
      -l tyler \
      proxy.example.com

Alternatively, Alex may add the following settings to his personal [`ssh_config(5)`](https://man.openbsd.org/ssh_config) and run `ssh -nN proxy`:

    Host proxy
      Hostname proxy.example.com
      Port 22
      User alex
      StrictHostKeyChecking yes
      IdentitiesFile ~/.ssh/id_proxy
      IdentitiesOnly yes
      RemoteForward 2222 localhost:22
      ExitOnForwardFailure yes
      RequestTTY no
    
    Host *
      Protocol 2
      AddKeysToAgent yes
      IgnoreUnknown UseKeychain
      UseKeychain yes


Tenth, Tyler connects to Alex' machine and forwards port 5900 from his machine to Alex' machine:

    $ ssh \
      -J tyler@proxy.example.com \
      -L 5900:localhost:5900 \
      -o "ExitOnForwardFailure yes" \
      -o "StrictHostKeyChecking yes" \
      -l alex \
      -p 2222 \
      -o "HostKeyAlias alex_workstation" \
      localhost

Alternatively, Tyler may add the following settings to his personal [`ssh_config(5)`](https://man.openbsd.org/ssh_config) and run `ssh alex_workstation`:

    Host alex_workstation
      ProxyJump tyler@proxy.example.com
      User alex
      Hostname localhost
      Port 2222
      HostKeyAlias alex_workstation
      StrictHostKeyChecking yes
      ExitOnForwardFailure yes
      LocalForward 5900 localhost:5900
    
    Host *
      AddKeysToAgent yes
      IgnoreUnknown UseKeychain
      UseKeychain yes

Finally, Tyler accesses the tunneled RFD server from his machine.
For example, using TigerVNC's [`vncviewer(1)`](https://tigervnc.org/doc/vncviewer.html)[^vncviewer]

    $ vncviewer localhost

[^vncviewer]:
Apparently, the [TigerVNC](https://tigervnc.org/)'s client, [`vncviewer(1)`](https://tigervnc.org/doc/vncviewer.html), may not connect to an [Apple Remote Desktop](https://support.apple.com/remote-desktop) agent, even if you enable the legacy VNC option of the latter.
However, you can use [FreeRDP](https://www.freerdp.com/) instead, or one of its graphical front-ends like [Gnome Boxes](https://wiki.gnome.org/Apps/Boxes).

TODO: permitlisten="none",permitopen="none"

## Conclusion

We've established a secure, end-to-end encrypted tunnel between two otherwise disconnected machines through an OpenSSH forward proxy.
Moreover, the two parties need minimal trust in each other because we've restricted the keys' permissions.

OpenSSH enables even more fine grained control if need be.
For example:

* Apply an `expiry-time` to your `authorized_keys` to grant temporary access.

* Add a trusted `cert-authority` to your `authorized_keys` instead of individual keys.

* Enable `VerifyHostKeyDNS` to automatically trust host keys with a corresponding SSHFP resource record in the DNS.

* Use [Unix domain sockets](https://en.wikipedia.org/wiki/Unix_domain_socket) instead of network ports to further restrict access to the tunnel from the proxy to the actual server.[^socket]

[^socket]:
Simply replace the port number with an absolute path like `/home/tyler/proxy.sock`.
Do not use the `~` to represent the user's home directory.

# Rewrite

## Goal

Forward a (protected) Unix domain socket on the proxy to a local port.
Such that nobody else on the proxy may use the forwarding, as with a forwarded TCP port.

Serve the current directory at http://127.0.0.1:8080/:

    python3 -m http.server --bind 127.0.0.1 8080

Using explicit loopback address 127.0.0.1 (or ::1) instead of localhost lest socat or ssh shoud bind to a non-loopback address.

Want either end-to-end encryption (using SSH server on the server) or protected address (Unix domain socket instead of TCP port) on the proxy.

## OpenSSH RemoteForward

Works as advertised.
Missing `PermitListen` (and `permitlisten`) equivalent to restrict the name of the socket.

## OpenBSD netcat

Idea: manually bind to remote socket using netcat.
Problem: Doesn't even work locally. Neither on Arch Linux nor on OpenBSD.

Forward Unix domain socket test.sock to TCP port 8080 using OpenBSD's [`nc`](https://man.openbsd.org/nc):

    #! /bin/sh
    rm -f backpipe
    mkfifo backpipe
    nc -lkU test.sock 0<backpipe \
      | nc 127.0.0.1 8080 1>backpipe

Retrieve the home page http://127.0.0.1:8080/ via the socket:

    printf "GET / HTTP/1.0\r\n\r\n" \
      | nc -UN test.sock

Unfortunately, this doesn't work reliably.
The retrieval command prints the response at most once, ofter on the second invocation and NOT on the first invocation.

I found neither the cause of this behavior nor a workaround.
I suspect, the problem is a combination of the following:

* Netcat closes connection to early because of an early EOF

* Shell pipe errors

* Shell buffers standard input/output

Moreover, each Linux appears to implement a slightly different version of OpenBSD's netcat.

## socat

Forward remote socket test.sock to local port 8080:

    socat EXEC:'ssh -T engine.skreutz.com socat "UNIX-LISTEN:test.sock,fork,unlink-early" STDIO' TCP4:127.0.0.1:8080,fork

TODO: Test local nc and remote socat, because it might be easier to find a nc implementation for Windows than socat.

Forward local port 8081 to remote socket test.sock:

    ssh -vnNT -L 127.0.0.1:8081:/home/stefan/test.sock -o "ExitOnForwardFailure yes" engine.skreutz.com

Note: Specify the local bind address 127.0.0.1 and option `ExitOnForwardFailure` to make `ssh` fail if `GatewayPorts` is set to `yes`.

Noto: Specify the absolute path of the socket. Do not rely on the ~.

Retrieve home page:

    printf "GET / HTTP/1.0\r\n\r\n" \
      | nc -N 127.0.0.1 8081

Alternatively:

    curl http://127.0.0.1:8081/

TODO: Restrict authorized_keys
TODO: Inspect ports with netstat.
TODO: permitlisten="none",permitopen="none".
TODO: Request OpenSSH feature PermitStreamLocalListen/Open analoguous to PermitListen/Open.
TODO: Test tcpserver instead of socat: https://cr.yp.to/ucspi-tcp.html
Generated by cgit. See skreutz.com for my tech blog and contact information.