SSH allows secure connections from one host to another. All traffic is encrypted. Authentication is usually by means of a key pair, where the private key resides on your local machine, and the public key is imported to the remote system. SSH keys have become particularly important for cloud computing, where users need to access cloud servers over a potentially hostile Internet.
Sometimes, the requirement is to access one system via another. You “hop” through the first system to reach the second. For example, an AWS server through a gateway or “jump” system. The following article shows how to do that, in a secure way, either by proxying or agent forwarding, without having to place a private SSH key onto the middle system.
SSH Key Arrangements
You could just create two SSH key pairs: one pair for access from A to B and the second to access C from B. However this involves installing a private SSH key onto the middle (potentially public) system, which is not desirable. An intruder who compromises server B might be able to read the private key there, and use it to access server C.
Instead, use only one key pair. Put the public key on B and C. Leave the private key on A only. This is the best and most secure arrangement. Using it will require SSH agent forwarding or SSH proxying. Either will do. The rest of this article explains how to do both.
SSH Agent Forwarding
We will cover agent forwarding first because it is better known. These examples assume that your user name on all 3 systems is “frederick“. You can use different user names on each system, just substitute as necessary.
With the private key on your local system (located in your home directory under .ssh/id_rsa), and the public key installed on both intermediate system B and target C (in authorized_hosts), proceed as follows.
Start the ssh-agent process.
frederick@hostA:~$ eval `ssh-agent`
Add your key information to the running agent:
frederick@hostA:~$ ssh-add ~/.ssh/id_rsa Enter passphrase for /home/frederick/.ssh/id_rsa: Identity added: /home/frederick/.ssh/id_rsa (/home/frederick/.ssh/id_rsa)
Check it by listing keys contained in the agent:
frederick@hostA:~$ ssh-add -l 2048 9e:db:5f:e0:92:b5:d4:67:2e:3a:ee:b4:32:b7:0e:28 /home/frederick/.ssh/id_rsa (RSA)
Next, use ssh with the -A flag to get onto system B.
frederick@hostA:~$ ssh -A -l frederick -i ~/.ssh/id_rsa hostB
where “hostB” is the IP address or resolvable host name of system B. Next, SSH into C, where “hostC” is the IP address or hostname of target system C:
frederick@hostB:~$ ssh -A C Enter passphrase for key '/home/frederick/.ssh/id_rsa': ... frederick@C:~ $
Success! You are logged into system C.
Notes:
- The above method can be repeated through as many intervening systems as necessary, so long as your public key is installed.
- The key passphrase is entered when the private key is added to the agent, and does not have to be re-entered during the subsequent SSH.
- It is not necessary to start an ssh-agent process on any system other than A, your local system.
- The ssh-agent process on A will continue to run after you log out of all systems. Don’t forget to kill it.
Once the key has been added to the local ssh-agent, a “authentication token” is forwarded by the -A flag of the SSH command, telling the remote systems that you have already authenticated with the private key.
SSH Proxying
It is possible to connect to an end system via an intermediate one with a single command, with no agent involvement and no authentication forwarding. This SSH command will do the whole lot in one big jump. As in the above example, the public key should be on B and C, with the corresponding private key on A only:
$ ssh -l frederick -oProxyCommand='ssh -l frederick -i ~/.ssh/id_rsa hostB nc hostC' hostC Enter passphrase for key '/home/frederick/.ssh/id_rsa': Enter passphrase for key '/home/frederick/.ssh/id_rsa':
It is two ssh commands. The “inner” one, within single speech marks, starts a netcat (“nc”) process on intermediate server B, which acts as a proxy, carrying data between B and the target server C. This is connected to the “outer” ssh (outside the single speech marks). The two combine to take your input via server B to server C, and bring output back again. You have to type in the passphrase twice: once for each SSH process.
The “outer” ssh is directed at hostC, even though we have no direct access to that host. Note that you must type the name (or IP address) of hostC two times: once for the “inner” SSH and once again for the “outer”.
Example
If you need to specify non-standard SSH ports (ie. your SSH daemon is running on a port other than port 22), use the “-p” option with SSH, and potentially pass the port number to netcat too. For example, here is an example of accessing a system within an organisation, whose SSH “gateway” runs on port 1234 and the target system runs SSH on port 5678:
$ ssh -p 5678 -oProxyCommand='ssh -p 1234 -i ~/.ssh/id_rsa <outer ip> nc <inner ip> 5678' <inner ip>
Note: “nc” must be installed on B for this to work. nc is not installed on all systems by default.
Improved Commands
More recent versions of SSH include a “-W” switch, obsoleting the need for nc:
frederick@A:~$ ssh -l frederick -oProxyCommand='ssh -l frederick -i ~/.ssh/id_rsa hostB -W hostC' hostC
In fact the inner “hostC” can be replaced with the -W special flag %h, which translates to the host to connect to, further tidying up the syntax a bit:
frederick@A:~$ ssh -l frederick -oProxyCommand='ssh -l frederick -i ~/.ssh/id_rsa hostB -W %h' hostC
If ssh is running on a non standard port, there is also a “%p” that can be used with the -W switch:
frederick@A:~$ ssh -l frederick -oProxyCommand='ssh -l frederick -i ~/.ssh/id_rsa hostB -W %h:%p' hostC
Example
Repeating the example above with the newer syntax:
$ ssh -p 5678 -oProxyCommand='ssh -p 1234 -i ~/.ssh/id_rsa <outer ip> -W %h:%p' <inner ip>
The %h variable just inserts the <inner ip> after the “-W”, which it obtains from the address at the end of the command. While %p translates to the port number of the server inside the network, 5678.
Notes
The proxying example uses a single key pair. It is, of course, possible to use two key pairs instead, one for system B and the other for C. In which case, the corresponding private keys should both be installed on A, with one public key on B and the other on C. The two passphrase prompts will pertain to each pair and might require different passphrases to be entered.
Proxying vs Agent Forwarding
Why is proxying better and more secure than agent forwarding? Well, first you don’t have to bother running an agent on the originating system and adding a key to it. Secondly and more importantly, because there is no agent, there is no socket file created. To recap, when you run an agent, a socket file is created and envrironment variables are set accordingly.
frederick@A:~$ ssh-agent SSH_AUTH_SOCK=/tmp/ssh-JVE6e5qBu5O1/agent.27366; export SSH_AUTH_SOCK; SSH_AGENT_PID=27367; export SSH_AGENT_PID; echo Agent pid 27367; frederick@A:~$ ls -l /tmp/ssh-JVE6e5qBu5O1/agent.27366 srw------- 1 frederick frederick 0 Oct 24 15:37 /tmp/ssh-JVE6e5qBu5O1/agent.27366
Unfortunately, another user (on system A) can set the same variables and then ssh to the endpoint, using the key, and without having to enter the key passphrase, ie. they can use the forwarded authentication token. Permissions on the socket file would normally prevent it, but not for somebody with root rights, who could also acquire and set the neccessary variables. I have successfully tested the procedure, but won’t put further details here.
With proxying there is no socket file and no associated vulnerability.
Conclusion
SSH Proxting and agent forwarding both work. Proxying more secure and slightly more convenient.