In tunneling, or port forwarding, a local port is connected to a port on a remote host or vice versa. So connections to the port on one machine are in effect connections to a port on the other machine.
The ssh(1) options -f (go to background), -N (do not execute a remote program) and -T (disable pseudo-tty allocation) can be useful for connections that are used only for creation of tunnels.
In regular port forwarding, connections to a local port are forwarded to a port on a remote machine. This is a way of securing an insecure protocol or of making a remote service appear as local. Here we forwarded VNC in two steps. First make the tunnel:
$ ssh -L 5901:localhost:5901 -l fred desktop.example.org
In that way connections on the local machine made to the forwarded port will in effect be connecting to the remote machine.
Multiple tunnels can be specified at the same time. The tunnels can be of any kind, not just regular forwarding. See the next section below for reverse tunnels. For dynamic forwarding see the section Proxies and Jump Hosts.
$ ssh -L 5901:localhost:5901 \ -L 5432:localhost:5432 \ -l fred desktop.example.org
If a connection is only used to create a tunnel, then it can be told not to execute any remote programs (-N), making it a non-interactive session, and also to drop to the background (-f).
$ ssh -fN -L 3128:localhost:3128 -l fred server.example.org
Note that -N will work even if the authorized_keys forces a program using the command="..." option. So a connection using -N will stay open instead of running a program and then exiting.
The three connections above could be saved in the SSH client's configuration file, ~/.ssh/config and even given shortcuts.
Host desktop desktop.example.org HostName desktop.example.org User fred LocalForward 5901 localhost:5901 Host postgres HostName desktop.example.org User fred LocalForward 5901 localhost:5901 LocalForward 5432 localhost:5432 Host server server.example.org HostName server.example.org User fred ExitOnForwardFailure no LocalForward 3128 localhost:3128 Host * ExitOnForwardFailure yes
With those settings, the tunnels listed are added automatically when connecting to desktop, desktop.example.org, postgres, server, or server.example.org. The catchall configuration at the end applies to any of the above hosts which have not already set ExitOnForwardFailure to 'no' and the client will refuse to connect if a tunnel cannot be made. The first obtained value for any given configuration directive will be used, but the file's contents can be overidden with run-time options passed on the command line.
Tunneling Via A Single Intermediate Host
Tunneling can go via one intermediate host to reach a second host, and the latter does not need to be on a publicly accessible network. However, the target port on the second remote machine does have to be accessible on the same network as the first. Here, 192.168.0.101 and bastion.example.org must be on the same network and, in addition, bastion.example.org has to be directly accessible to the client machine running ssh(1). So, port 80 on 192.168.0.101 has to be available to the machine bastion.example.org.
$ ssh -fN -L 1880:192.168.0.101:80 -l fred bastion.example.org
Thus, once the tunnel is made, to connect to port 80 on 192.168.2.101 via the host bastion.example.org, connect to port 1880 on localhost. This way works for one or two hosts. It is also possible to chain multiple hosts, using different methods.
Securing a Hop, Tunneling Via One Or More Intermediate Hosts
Here, the idea is to limit the ability of a group of users to the bare minimum needed to pass through a jump host yet still be able to forward ports onward to other machines. If the account is sufficiently locked down then the bastion can only be used for fowarding and not shell access, scripts, or even SFTP. The following settings on the bastion host in sshd_config(5)prevent either shell access or SFTP but still allow port forwarding.
Match Group tunnelers ForceCommand /bin/false PasswordAuthentication no ChrootDirectory %h PermitTTY no X11Forwarding no AllowTcpForwarding yes PermitTunnel no Banner none
Note that their home directories, but not the files within them, must be owned by root and writable by only root because of the ChrootDirectory configuration directive there in sshd_config(5). Also, because of the PasswordAuthentication configuration directive keys will have to be set up in the home directory in ~/.ssh/authorized_keys, if an alternate location is not already configured.
$ ssh -N -L 9980:localhost:80 -J email@example.com firstname.lastname@example.org
In that way port 9980 on the client is directed via bastion.example.org through to port 80 on 192.168.79.124.
In cases where the bastion must have a reverse tunnel from the inner host in order to reach it, then the same method works but with the prerequisite of a reverse tunnel from the inner host to the bastion first.
For more about passing through intermediate computers, see the Cookbook section on Proxies and Jump Hosts.
Finding The Process ID (PID) Of A Tunnel Which Is In The Background
When a tunneled connection is sent to the background execution using the -f option for the client, there is not currently an automatic way to find the process ID (PID) of the task sent to the background. Background processes are often used for port forwarding or reverse port forwarding. Here is an example of port forwarding, also called tunneling. The connection is made and then the client goes away, leaving the tunnel available in the background, connecting port 2194 on the local host to port 194 of the remote system's local host.
$ ssh -Nf -L 2194:localhost:194 email@example.com
The special $! variable remains empty, even if $? reports success or failure of the action. The reason is that the shell's job control did not put the client into the background. Instead the client runs in the foreground for a moment and then exits normally, after leaving a different process to run in the background via a fork. The process ID of the original client vanishes since that client is gone.
Finding the process ID usually takes at least two steps. Some of the ways to retroactively identify the process involve trying ps(1) and rummaging through the output for all that account's processes, but that is uneccessary effort. If the background SSH client is the most recent one, then pgrep(1) can be used, or else the output needs to be comma delimited and fed into ps(1) via xargs(1) or process subsitution.
$ ps uw | less $ pgrep -n -x ssh $ pgrep -d, -x ssh | xargs ps -p $ ps -p $(pgrep -d, -x ssh)
Some variations on the above might be needed depending on operating system if the -d option is not supported.
$ pgrep -x ssh | xargs -n 1 ps -o user,pid,ppid,args -p | awk 'NR==1 || $3==1' USER PID PPID COMMAND fred 97778 1 ssh -fN -L 8008:localhost:80 firstname.lastname@example.org fred 14026 1 ssh -fN -L 8183:localhost:80 email@example.com fred 79522 1 ssh -fN -L 8228:localhost:80 firstname.lastname@example.org fred 49773 1 ssh -fN -L 8205:localhost:80 203.0.113.205
Either way, note that all the connections running in the background have a Parent Process ID (PPID) of 1, the process control initialization system for the operating system.
Being aware of this shortcoming, a proactive approach can be used with the ControlMaster and ControlPath configuration directives in order to leave a socket to read to get the background task's process ID.
$ ssh -M -S ~/.ssh/sockets/pid.%C -fN -L 5901:localhost:5901 email@example.com $ ssh -S ~/.ssh/sockets/pid.%C -O check 203.0.113.122
The -M option causes the client to go into Master mode for multiplexing using the socket designated by the -S option. Then, after -f forks the client into the background, The control command check will then use the socket to check that the master process is running and report the Process ID.
It is a good idea for the socket to be in an isolated directory not readable or writable by other accounts. Aside from the complexity, a noticeable down side is that it can be possible for the socket to be reused for additional connections. See the Cookbook section on Multiplexing for more about the risks and additional uses.
A reverse tunnel goes the opposite direction of a regular tunnel. In a reverse tunnel, a port on the remote host is forwarded to the local host. Once the connection is made, it works the same as a regular tunnel. Connections to the destination port on the local host connect to the remote host's port.
On the machine that will become the remote machine, open a reverse tunnel. Here a reverse tunnel is made from port 2022 on that soon-to-be remote machine to port 22 over on the local machine:
$ ssh -fNT -R 2022:localhost:22 -l fred server.example.org
Then over on the local machine, connecting to the forwarded port as the local host will open the connection to the machine hosting the reverse tunnel. Then using the example above a connection is made on the remote machine to the reverse tunnel connection on port 2022.
$ ssh -p 2022 -l fred localhost
A common use-case for reverse tunneling is when you have to access a machine that is behind a firewall that blocks incoming SSH connections but without changing the firewall settings, and you have direct access to a second machine outside the firewall. It is easy to make a reverse tunnel from the machine behind the firewall to the second machine. Then to connect to the first machine from outside, connect to the forwarded port on the second machine. The second machine on the outside acts as a relay server to forward connections to the machine on the inside.
Adding or Removing Tunnels within an Established Connection
It is possible to add or remove tunnels, reverse tunnels, and SOCKS proxies to or from an existing connection using escape sequences. The default escape character is the tilde (~) and the full range of options is described in the manual page for ssh(1). Escape sequences only work if they are the first characters entered on a line and followed by a return. When adding or removing a tunnel to or from an existing connection, ~C, the command line is used.
To add a tunnel in an active SSH session, use the escape sequence to open a command line in SSH and then enter the parameters for the tunnel:
~C L 2022:localhost:22
To remove a tunnel from an active SSH session is almost the same. Instead of -L, -R, or -D we have -KL, -KR, and -KD plus the port number. Use the escape sequence to open a command line in SSH and then enter the parameters for removing the tunnel.
Adding or Removing Tunnels within a Multiplexed Connection
There is an additional option for forwarding when multiplexing. More than one SSH connection can be multiplexed over a single TCP connection. Control commands can be passed to the master process to add or drop port forwarding to the master process.
First a master connection is made and a socket path assigned.
$ ssh -S '/home/fred/.ssh/%h:%p' -M server.example.org
Then using the socket path, it is possible to add port forwarding.
$ ssh -O forward -L 2022:localhost:22 -S '/home/fred/.ssh/%h:%p' firstname.lastname@example.org
Since OpenSSH 6.0 it is possible to cancel specific port forwarding using a control command.
$ ssh -S "/home/fred/.ssh/%h:%p" -O cancel -L 2022:localhost:22 email@example.com
For more about multiplexing, see the Cookbook section on Multiplexing.
Restricting Tunnels to Specific Ports
By default, port forwarding will allow forwarding to any port if it is allowed at all. The way to restrict which ports a user can use in forwarding is to apply the PermitOpen option on the server side either in the server's configuration or inside the user's public key in authorized_keys. For example, with this setting in sshd_config(5) all users can forward only to port 7654 on the server, if they try forwarding:
Multiple ports may be specified on the same line if separated by whitespace.
PermitOpen localhost:7654 localhost:3306
If the client tries to forward to a disallowed port, there will be a warning message that includes the text "open failed: administratively prohibited: open failed" while the connection otherwise continues as normal. However, even if the client has ExitOnForwardFailure in its configuration a connection will still succeed, despite the warning message.
However, if shell access is available, it is possibile to run other port forwarders, so without further restrictions, PermitOpen is more of a reminder or recommendation than a restriction. But for many cases, such a reminder might be enough.
The PermitOpen option can be used as part of one or more Match blocks if forwarding options need to vary depending on various combinations of user, group, client address or network, server address or network, and/or the listening port used by sshd(8). If using the Match criteria to selectively apply rules for port forwarding, it is also possible to prevent the account from getting an interactive shell by setting PermitTTY to no. That will prevent the allocation of a pseudo-terminal (PTY) on the server thus prevent shell access, but allow other programs to still be run unless an appropriate forced command is specified in addition.
Match Group mariadbusers PermitOpen localhost:3306 PermitTTY no ForceCommand /usr/bin/true
With that stanza in sshd_config(5) it is only possible to connect by adding the -N option to avoid executing a remote command.
$ ssh -N -L 3306:localhost:3306 server.example.org
The -N option can be used alone or with the -f option which drops the client to the background once the connection is established.
Without the ForceCommand option in a Match block in the server configuration, if an attempt is made to get a PTY by a client that is blocked from getting one, the warning "PTY allocation request failed on channel n" will show up on the client, with n being the channel number, but otherwise the connection will succeed without a remote shell and the port(s) will still be forwarded. Various programs, including shells, can still be specified by the client, they just won't get a PTY. So to really prevent access to the system other than forwarding, a forced command is needed. The tool true(1) comes in handy for that. Note that true(1) might be in a different location on different systems.
Limiting Port Forwarding Requests Using Keys Only
The following authorized_keys line shows the PermitOpen option prepended to a key in order to limit a user connecting with that particular key to forwarding to just port 8765:
permitopen="localhost:8765" ssh-ed25519 AAAAC3NzaC1lZDI1NT...
Multiple PermitOpen options may be applied to the same public key if they are separated by commas and thus a key can allow multiple ports.
By default, shell access is allowed. With shell access it is still possible to run other port forwarders. The no-pty option inside the key can facilitate making a key that only allows forwarding and not an interactive shell if combined with an appropriate forced command using the command option. Here is an example of such a key as it would be listed in authorized_keys:
no-pty,permitopen="localhost:9876",command="/usr/bin/true" ssh-ed25519 AAAAC3NzaC1lZDI1NT...
The no-pty option blocks interactive shell. The client will still connect to the remote server and will allow forwarding but will respond with an error including the message "PTY allocation request failed on channel n". But as mentioned in the previous subsection, there are still a lot of ways around that and adding the command option hinders them.
This method is awkward to lock down. If the account has write access in any way, directly or indirectly, to the authorized_keys file, then it is possible for the user to add a new key or overwrite the existing key(s) with more permissive options. In order to prevent that, the server has to be configured to look for the file in a location outside of the user's reach. See the section on Public Key Authentication for details on that. The methods described above in the previous subsection using sshd_config(5) might be more practical in many cases.