Services Blog Français

Firewall Bypass Techniques with Bastions, Tunnels, and Proxies

| by jpic | devops network linux

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.

Table of Contents

We will explore the following techniques:

  • SOCKS proxy to encapsulate any TCP connection via an SSH connection
  • Dante sockd: opening a SOCKS proxy port that allows traffic from the local machine to pass through a remote machine
  • Forwarding a local port to a remote machine via SSH: local port 1234 becomes accessible via port 1234 on the remote machine
  • Conversely, forwarding a remote machine’s local port via SSH: remote port 1234 becomes accessible via port 1234 on the local machine
  • sshuttle: the super cute tool that allows forwarding networks accessible from a remote machine to a local machine

And how to combine several of these techniques.

SOCKS Proxy with SSH

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!

SOCKS Proxy Dante

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.

Port Forwarding: Local to Remote

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.

Port Forwarding: Remote to Local

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

Dante sockd + SSH Port Forwarding

Combining what we have learned, here is our plan:

  • open a SOCKS proxy port on port 1337 of our bastion
  • bring this port back so it is accessible as localhost from our dev machine

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 use
  • make sure AllowTcpForwarding is set to yes in this file
  • look for DenyUser and make sure your user is not in DenyUser, as if it is, you will need to remove the line
  • while you’re at it, set LogLevel to DEBUG, this will be useful if you need to debug
  • also, check the value of AuthorizedKeysFile, by default it’s .ssh/authorized_keys, but it can vary in corporate configurations
  • finally, start or reload your local ssh server

Then, 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

Bonus: sshuttle

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!

They trust us

Contact

logo