Paramiko: Validating SSH Host Keys

Author: Kirk Byers
Date: 2015-12-07

Previously, I wrote an article about using Parmiko SSH to connect to network devices. In that earlier article, I used the following code (with a disclaimer):

remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy())

The above code causes Paramiko to blindly accept the SSH host key from the remote device. Conseqently, this makes you vulnerable to a man-in-the-middle attack. For network engineers, the risk is compounded since we are frequently using passwords (and not SSH keys) for authentication.

This is not very good...

But how can we improve this situation? How can we verify the remote host's identity?

First let's look at Paramiko's default behavior which is to reject unknown SSH host keys (in other words, what happens if we don't execute .set_missing_host_key_policy(paramiko.AutoAddPolicy())

>>> import paramiko
>>> from getpass import getpass
>>> 
>>> host = '10.10.10.27'
>>> username = 'pyclass'
>>> passwd = getpass()
Password: 
>>> 
>>> remote_conn_pre=paramiko.SSHClient()
>>> remote_conn_pre.connect(hostname=host, port=22, username=username,
...   password=passwd, look_for_keys=False, allow_agent=False)
Traceback (most recent call last):
  File "", line 2, in 
  File "/home/admin/VENV/py27_netmiko/local/lib/python2.7/site-packages/paramiko/client.py", line 288, in connect
    server_key)
  File "/home/admin/VENV/py27_netmiko/local/lib/python2.7/site-packages/paramiko/client.py", line 570, in missing_host_key
    raise SSHException('Server %r not found in known_hosts' % hostname)
paramiko.ssh_exception.SSHException: Server '10.10.10.27' not found in known_hosts

Note, I have modified the output captures to hide certain information (for example, the remote IP address).

So, as you can see, Paramiko will reject an unknown remote host by default. Now at the SSH level let's add the remote device to known hosts:

$ ssh -l pyclass 10.10.10.27
The authenticity of host '10.10.10.27 (10.10.10.27)' can't be established.
RSA key fingerprint is aa:bb:cc:dd:22:ff:dd:ee:44:55:99:22:88:88:aa:00.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '10.10.10.27' (RSA) to the list of known hosts.
Password:

And then back in Python let's load the known_hosts for that user (by default from ~/.ssh/known_hosts). We do this by calling the 'load_system_host_keys()' method:

>>> remote_conn_pre.load_system_host_keys()

Now let's try our SSH connection again

>>> remote_conn_pre.connect(hostname=host, port=22, username=username,
...   password=passwd, look_for_keys=False, allow_agent=False)
>>> remote_conn = remote_conn_pre.invoke_shell()
>>> output = remote_conn.recv(1000)
>>> print output

pynet-rtr1#

Now, we are successfully connecting to the router including verifying the identity of the remote device. The identity verification is based on the SSH host key found in ~/.ssh/known_hosts.

Besides invoking the .load_system_host_keys() method, you can also use the .load_host_keys() method. This would allow you to have a common system host keys file that you distributed to multiple systems and a local host keys file (that was specific to this one machine)

Let's create a new Paramiko SSH Client object:

remote_conn_pre2 = paramiko.SSHClient()

And make sure that this fails by default:

>>> remote_conn_pre2.connect(hostname=host, port=22, username=username,
...   password=passwd, look_for_keys=False, allow_agent=False)
Traceback (most recent call last):
  File "", line 2, in 
  File "/home/admin/VENV/py27_netmiko/local/lib/python2.7/site-packages/paramiko/client.py", line 288, in connect
    server_key)
  File "/home/admin/VENV/py27_netmiko/local/lib/python2.7/site-packages/paramiko/client.py", line 570, in missing_host_key
    raise SSHException('Server %r not found in known_hosts' % hostname)
paramiko.ssh_exception.SSHException: Server '10.10.10.27' not found in known_hosts

Now let's create a new known hosts file with just the one router entry in it:

$ ssh -l pyclass -o UserKnownHostsFile=./cisco_known_hosts 10.10.10.27
The authenticity of host '10.10.10.27 (10.10.10.27)' can't be established.
RSA key fingerprint is aa:bb:cc:dd:ee:ff:11:22:33:44:55:66:77:88:99:aa.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '10.10.10.27' (RSA) to the list of known hosts.
Password:  

$ ls -ltr cisco_known_hosts 
-rw-r--r-- 1 admin admin 442 Dec  4 12:48 cisco_known_hosts

Now we have this file, let's use it in Python:

>>> from os import path 
>>> home_dir = (path.expanduser('~'))
>>> ssh_file = home_dir + '/.ssh/cisco_known_hosts'
>>> remote_conn_pre2.load_host_keys(ssh_file)

Now let's try our SSH connection again:

>>> remote_conn_pre2.connect(hostname=host, port=22, username=username,
...   password=passwd, look_for_keys=False, allow_agent=False)
>>> 
>>> remote_conn2 = remote_conn_pre2.invoke_shell()
>>> output = remote_conn2.recv(1000)
>>> print output

pynet-rtr1#

So once again we are in business...this time using an alternate known hosts file.

Using the above methods we are able to verify the identity of remote devices and thus reduce the man-in-the-middle security risk. Obviously, you must still find a secure way to obtain the remote SSH host keys to begin with. Periodically dealing with this issue, however, is much better than just blindly trusting the remote SSH host key on every SSH connection.

Kirk Byers

@kirkbyers