Services
Blog
Français
A typical corporate network does not allow admins to directly connect to the machines they administer, requiring connection through an intermediary machine: the bastion.
This guide cannot be exhaustive as there are numerous techniques to bypass this type of limitation. Here, we will focus on presenting various SSH options, as well as a separate SOCKS proxy, specifically Dante (sockd), and how to combine them in various enterprise scenarios.
A SOCKS proxy (Socket Secure) is a session-layer protocol (layer 5 of the OSI model) that allows redirecting any TCP stream, and sometimes UDP, through an intermediary server without the client application needing to know the final destination.
We will explore the following techniques:
And how to combine several of these techniques.
The simplest way to set up a proxy, when the remote server allows it, is to use ssh -D, an excerpt from the man ssh:
-D [bind_address:]port
Enables a local “dynamic” port forwarding at the application level (Dynamic Port Forwarding). This involves allocating a listening socket on the specified port on the local machine (the client), optionally bound to a specific address via bind_address.
As soon as a connection arrives on this local port, it is transmitted through the secure SSH tunnel, and then the connecting application (usually a browser or any other client configured to use a SOCKS proxy) decides on the final destination.
OpenSSH implements a true SOCKS proxy: both SOCKS4 and SOCKS5 protocols are supported, and ssh acts as a SOCKS proxy server relaying requests to the destinations requested by the client.
[…]
By default, the listening port is bound according to the GatewayPorts parameter.
So, in a terminal, I connect to my server with -D 1236:
09/12 2025 13:51:23 jpic@jpic ~
$ ssh -D 1236 ci.yourlabs.io
[jpic@ci ~]$
And in another terminal, I still exit with my IP:
09/12 2025 13:51:30 jpic@jpic ~
$ curl ipconfig.io
98.110.80.162
But, if I go through the SOCKS proxy with the ALL_PROXY environment variable, then I exit with the IP of my remote server:
09/12 2025 13:51:44 jpic@jpic ~
$ ALL_PROXY=socks5://localhost:1236 curl ipconfig.io
163.172.69.187
So, it’s clearly the simplest and most practical option, but some corporate SSH servers do not allow changing the sshd configuration, so we will need to resort to other practices.
Indeed, the server may refuse because AllowTcpForwarding is set to no in sshd_config, in which case the manipulations will look like this:
09/12 2025 14:00:52 jpic@jpic ~
$ ssh -D 1236 ci.yourlabs.io
[jpic@ci ~]$ channel 3: open failed: administratively prohibited: open failed
$ ALL_PROXY=socks5://localhost:1236 curl ipconfig.io
curl: (97) Failed to receive SOCKS response, proxy closed connection
But no panic, we will use Dante sockd and forward the port from the bastion to our local machine to benefit from the same functionality!
dante-server is a package available on RHEL, for example, so we can install it and set up a minimal configuration:
logoutput: stderr
internal: 127.0.0.1 port = 1337
external: <the IP of the bastion>
debug: 2
clientmethod: none
socksmethod: none
client pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
}
socks pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
}
Which we put in a file, say dante.conf, and allows us to start the proxy server with the following command:
sockd -f dante.conf
This opens a SOCKS proxy on port 1337 of the bastion, which is completely useless until we find a way to forward this port from the bastion to a local port.
We will therefore look at two techniques to achieve this, then we will connect the two ends with an example.
This is simply the -R option of the ssh command, to forward a local port to a remote port, here is an excerpt from the documentation of this option in man ssh:
-R [bind_address:]port:host:hostport
-R [bind_address:]port:local_socket
-R remote_socket:host:hostport
-R remote_socket:local_socket
-R [bind_address:]port
Indicates that connections arriving on a given TCP port or Unix socket on the remote side (SSH server) should be redirected to the local machine (SSH client).
This works by creating a listening socket on the remote machine, either on a TCP port or on a Unix socket. As soon as a connection arrives on this remote port or socket, it is transmitted through the secure SSH tunnel, and then a connection is established from the local machine (the client) to the indicated destination.
To test, nothing simpler, we start a small server locally on port 1234 with the command:
09/12 2025 13:26:52 jpic@jpic ~
python3 -m http.server 1234
Then, we find a target server to which we connect like this:
$ ssh -R 1234:localhost:1234 ci.yourlabs.io
And we see that port 1234 of the jpic machine is now accessible from port 1234 of the ci machine:
[jpic@ci ~]$ curl -I localhost:1234
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.13.7
Date: Tue, 09 Dec 2025 12:33:39 GMT
Content-type: text/html
Content-Length: 4154
Last-Modified: Thu, 27 Feb 2025 21:49:05 GMT
However, this can also fail:
$ ssh -R 1234:localhost:1234 ci.yourlabs.io
Warning: remote port forwarding failed for listen port 1234
[jpic@ci ~]$
We can sometimes see the problem by re-running the ssh command with -v:
09/12 2025 13:34:49 jpic@jpic ~
$ ssh -v -R 1234:localhost:1234 ci.yourlabs.io
debug1: OpenSSH_10.2p1, OpenSSL 3.6.0 1 Oct 2025
[...]
debug1: Remote: Server has disabled port forwarding.
debug1: remote forward failure for: listen 1234, connect localhost:1234
Warning: remote port forwarding failed for listen port 1234
debug1: pledge: network
And there it’s quite clear: the server refuses TCP forwarding. The solution: change AllowTcpForwarding no to AllowTcpForwarding yes in the file /etc/ssh/sshd_config and reload the sshd service with systemctl restart sshd.
In the case of a corporate network, it may be a Centrify sshd server, in which case the configuration will rather be in /etc/centrifydc/ssh/sshd_config and the service to reload centrify-sshd.
To perform the reverse operation, it’s ssh -L:
-L [bind_address:]port:host:hostport
-L [bind_address:]port:remote_socket
-L local_socket:host:hostport
-L local_socket:remote_socket
Indicates that connections to a given TCP port or Unix socket on the local host (the client) should be redirected to the specified host and port, or to the indicated Unix socket, on the remote side (SSH server).
This works by allocating a listening socket, either on a local TCP port (optionally bound to a specific bind address with bind_address), or on a local Unix socket.
As soon as a connection arrives on this local port or socket, it is transmitted through the secure SSH tunnel, and then a new connection is established from the remote machine to the host:port (hostport) or to the remote_socket Unix socket indicated.
To try, we connect like this and then start a server on port 1235 of the remote server:
$ ssh -L 1235:localhost:1235 ci.yourlabs.io
[jpic@ci ~]$ python -m http.server 1235
Serving HTTP on 0.0.0.0 port 1235 (http://0.0.0.0:1235/) ...
And as you can see, we can establish a connection on port 1235 of the local machine to access port 1235 of the remote machine:
09/12 2025 13:42:33 jpic@jpic ~
$ curl -I localhost:1235
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.13.7
Date: Tue, 09 Dec 2025 12:34:21 GMT
Content-type: text/html; charset=utf-8
Content-Length: 2842
If AllowTcpForwarding no, then you will see something like this on the server:
$ ssh -L 1235:localhost:1235 ci.yourlabs.io
[jpic@ci ~]$ python -m http.server 1235
Serving HTTP on 0.0.0.0 port 1235 (http://0.0.0.0:1235/) ...
channel 3: open failed: administratively prohibited: open failed
And obviously, a connection error locally:
$ curl -I localhost:1235
curl: (56) Recv failure: Connection reset by peer
Combining what we have learned, here is our plan:
The first step will be to be able to connect from the bastion to our dev machine, in reverse. So, this is what we will try first by following these steps on your dev machine:
find /etc -name sshd_config to find the path to our sshd configuration file, if you see a path like /etc/centrifydc/ssh/sshd_config then that’s probably the one to useAllowTcpForwarding is set to yes in this fileDenyUser and make sure your user is not in DenyUser, as if it is, you will need to remove the lineLogLevel to DEBUG, this will be useful if you need to debugAuthorizedKeysFile, by default it’s .ssh/authorized_keys, but it can vary in corporate configurationsThen, still on your dev machine, create an SSH key with the following command, for example:
ssh-keygen -t ed25519 -a 100
It should land in ~/.ssh/id_ed25519, and the public key in ~/.ssh/id_ed25519.pub. To be able to connect from your bastion to your dev machine, you will need to send the key pair to the bastion, but also add the public key in .pub to the AuthorizedKeysFile.
If AuthorizedKeysFile is set to .ssh/authorized_keys, then proceed like this:
cat ~/.ssh/id_ed25519.pub >> .ssh/authorized_keys
# and, very important for sshd to read the file, set it to user-writable only:
chmod 600 .ssh/authorized_keys
But, if you have, for example, AuthorizedKeys /etc/ssh/keys/%u.key, then you should proceed like this:
cat ~/.ssh/id_ed25519.pub | sudo tee -a /etc/ssh/keys/${USER}.key
sudo chmod 600 /etc/ssh/keys/${USER}.key
Then, copy your key pair to your bastion:
scp .ssh/id_ed* mon-bastion:/tmp
Next, you must absolutely be able to connect from your bastion to your local server, try like this:
[ma-dev] $ ip a # look at the IP of ma-dev
[ma-dev] $ ssh mon-bastion
[mon-bastion] $ ssh -i /tmp/id_ed25519 mon-uid@mon-ip
If it doesn’t work, look at the sshd server logs on your dev machine with one of the following commands, depending on your sshd server:
sudo journalctl -fu sshd
# or, in the case of Centrify:
sudo journalctl -fu centrify-sshd
If LogLevel is set to DEBUG on your sshd server, then the error should be clearly indicated in these logs.
Once done, start a multiplexer like tmux or screen, in a first terminal start the SOCKS proxy:
while :; do sockd -f dante.conf; done
And in a second terminal inside our multiplexer, connect to our machine by forwarding port 1337:
while :; do ssh -R 1337:localhost:1337 <our user>@<our dev>; done
Thus, on your dev machine, you can make your connections exit through the bastion with:
export ALL_PROXY=socks5://localhost:1337
It’s a cute pip package that allows forwarding all traffic from the machine to a bastion, TCP + UDP + DNS, for example with the following command:
sshuttle --dns -r user@server 0.0.0.0/0
I leave you to check out the README of the sshuttle project to discover all the cool things you can do!