GNU/Linux Crypto: SSH keys

This entry is part 4 of 10 in the series GNU/Linux Crypto.

The usual method of authenticating to an OpenSSH server is to type your shell password for the remote machine:

tom@local:~$ ssh remote
The authenticity of host 'remote (192.168.0.64)' can't be established.
RSA key fingerprint is d1:35:45:a6:d1:b2:e4:08:f8:67:b1:19:fe:04:ca:1c.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'remote,192.168.0.64' (RSA) to the list of known hosts.
tom@remote's password:

tom@remote:~$

This is appropriate for first contact with a machine, and the authentication method is supported out of the box for most OpenSSH sshd(8) installations.

sshd(8) is a very common target for attacks, particularly automated ones; malicious bots attempt to connect to servers listening on the standard SSH destination port of tcp/22, as well as some common alternatives like tcp/2222. If you enforce a strong password policy on your system, this generally isn’t too much of a problem, particularly if only appropriate users have shells, or if you restrict SSH connections only to certain usernames or groups.

There are other measures to defeat automated attacks, such as employing systems like fail2ban to reject clients who make too many spurious connection attempts, but perhaps the most effective way of short-circuiting automated attacks is to bypass passwords completely and instead use SSH keys, allowing this as the only connection method to the relevant machines.

How it works

Similar to the GnuPG keys setup in the first two articles in this series, SSH keypairs are comprised of one private key and one public key, two cryptographically linked files. The basis of keys for authentication is that if someone has your public key, they’re able to authenticate you by requesting operations that you would only be able to perform with the corresponding private key; it works similarly to cryptographic signing.

The reason this is so effective is because if you require a valid public key to authenticate, with sufficient key length it’s effectively impossible for an attacker to guess your authentication details; there’s no such thing as a “common” private key to guess, so they would need to run through every possible private key, which is not even remotely practical.

Your system’s sshd(8) may still be attacked, but if you use only public key authentication, then you can be comfortably certain it’s effectively impossible to brute-force your credentials. Note that this doesn’t necessarily protect you from security problems in sshd(8) itself, however, and you will still need to protect your private key from being hijacked or compromised, hence the necessity of a passphrase.

All of the below assumes you have OpenSSH installed as both the client and the server on the appropriate systems. On Debian-derived systems, these can be installed with:

# apt-get install ssh
# apt-get install openssh-server

Both the client and server often come standard with systems (e.g. their native OpenBSD).

Generating keys

Similar to the GnuPG setup process, we start by generating a keypair on the machine from which we’d like to connect, using ssh-keygen(1). I’m using 4096-bit RSA here, as it’s widely supported even on very old systems, and should be relatively future-proof, although generating new keys if RSA ever becomes unsafe is not hard. If you’d prefer to use the newer ECDSA algorithm that’s the default in recent versions of OpenSSH, all of this will still work. I’m also applying a comment for the key as an unencrypted identifier to distinguish multiple keys if I have them. I find email addresses work well.

$ ssh-keygen -t rsa -b 4096 -C tom@sanctum.geek.nz
Generating public/private rsa key pair.

First, we’re prompted for a location to which the key files should be saved. I recommend accepting the default by pressing Enter, as using the default location makes the next few steps easier:

Enter file in which to save the key (/home/tom/.ssh/id_rsa):

Next, we’re prompted for a passphrase, which should definitely be added to keep the key from being used if it’s ever compromised. The same guidelines for passphrases apply to SSH here, and you should choose a different passphrase:

Enter passphrase (empty for no passphrase):
Enter same passphrase again:

This done, the key is generated, including a pictorial representation to recognise keys at a glance. I’ve never found this very useful, but the key fingerprint is helpful:

Your identification has been saved in /home/tom/.ssh/id_rsa.
Your public key has been saved in /home/tom/.ssh/id_rsa.pub.
The key fingerprint is:
d5:81:8c:eb:c6:c5:a2:b9:6a:ae:32:cc:20:bf:cf:66 tom@local
The key's randomart image is:
+--[ RSA 4096]----+
|         o ..    |
|        . o. .   |
|         o. .    |
|        o.o      |
|       =So       |
|o     o +        |
|=.     o         |
|oo..E .          |
| ooO=.           |
+-----------------+

The key files should now be available in ~/.ssh:

$ ls -l .ssh
-rw-------  1 tom  tom  3326 Apr  2 22:47 id_rsa
-rw-r--r--  1 tom  tom   754 Apr  2 22:47 id_rsa.pub

The id_rsa file contains the encrypted private key, and should be kept locked down and confidential. The id_rsa.pub file, however, contains the public key, which can be safely distributed, in the same way as a PGP public key.

Key-based authentication

We can now arrange to use the newly generated public key for authentication in lieu of a password. Start by ensuring you can connect to the remote machine with your username and password:

$ ssh remote
tom@remote's password:

Once connected, ensure that the ~/.ssh directory exists on the remote machine, and that you don’t already have keys listed in ~/.ssh/authorized_keys, as we’re about to overwrite them:

$ mkdir -p ~/.ssh
$ chmod 0700 ~/.ssh

If this worked, close the connection (exit or Ctrl-D) to return to your local machine’s shell, and copy your public key onto the remote machine with scp(1):

$ scp ~/.ssh/id_rsa.pub remote:.ssh/authorized_keys
tom@remote's password:
id_rsa.pub    100%    754    0.7KB/s    00:00

Note that there’s a tool included in recent versions of OpenSSH that does this for you called ssh-copy-id(1), but it’s good to have some idea of what it’s doing in the background.

With this done, your next connection attempt to the remote host should prompt you for your passphrase, rather than your password:

$ ssh remote
Enter passphrase for key '/home/tom/.ssh/id_rsa':

Advantages

At first, it may not seem like you’ve done much useful here. After all, you still have to type in something to connect each time. From a security perspective, the first major advantage to this method is that neither your password, nor your passphrase, nor your private key are ever transmitted to the server to which you’re connecting; authentication is done purely based on the public-private key pair, decrypted by your passphrase.

This means that if the machine you’re connecting to were compromised, or your DNS had been poisoned, or some similar attack tricked you into connecting to a fake SSH daemon designed to collect credentials, your private key and your password remain safe.

The second advantage comes with turning off password authentication entirely on the host machine, once all its users have switched to public key authentication only. This is done with the following settings in sshd_config(5), usually in /etc/ssh/sshd_config on the remote server:

PubkeyAuthentication yes
ChallengeResponseAuthentication no
PasswordAuthentication no

Restart the SSH server after these are applied:

$ sudo /etc/init.d/ssh restart

You should then no longer be able to connect via passwords at all, only by private keys, which as mentioned above are effectively (though not literally) impossible to brute-force. In order to connect to the server as you, an attacker would not only need to know your passphrase, but also have access to your private key, making things significantly harder.

Using public key authentication also allows sshd(8) some finer-grained control over authentication, such as which hosts can connect with which keys, whether they can execute TCP or X11 tunnels, and (to an extent) which commands they can run once connected. See the manual page for authorized_keys(5) to take a look at some examples.

Finally, there’s a major usability advantage in using SSH keys for authentication with agents, which we’ll discuss in the next article.

Host keys and fingerprints

SSH connection should ideally be a two-way authentication process. Just as the server to which you’re connecting needs to be sure who you are, you need to be sure that the host you’re connecting to is the one you expect. With tunnelling, firewalls, DNS poisoning, NAT, hacked systems, and various other tricks, it’s appropriate to be careful that you’re connecting to the right systems. This is where OpenSSH’s host key system comes into play.

The first time you connect to a new server, you should see prompts like this:

$ ssh newremote
The authenticity of host 'newremote (192.168.0.65)' can't be established.
RSA key fingerprint is f4:4b:f4:8c:c5:50:f6:c8:d3:b2:e9:14:68:86:b5:7b.
Are you sure you want to continue connecting (yes/no)?

A lot of administrators turn these off; don’t! They are very important.

The key fingerprint is a relatively short hash for the host key used by OpenSSH on that server. It’s verified by your SSH client, and can’t easily be faked. If you’re connecting to a new server, it’s appropriate to check the host key fingerprint matches the one you see on first connection attempt, or to ask the system’s administrator to do so for you.

The host key’s fingerprint can be checked on the SSH server with a call to ssh-keygen(1):

$ ssh-keygen -lf /etc/ssh/ssh_host_rsa_key
2048 f4:4b:f4:8c:c5:50:f6:c8:d3:b2:e9:14:68:86:b5:7b /etc/ssh/ssh_host_rsa_key.pub (RSA)

If you want, you can check the key without making a connection attempt with a similar call on the client side:

$ ssh-keygen -lF newremote
# Host 192.168.0.65 found: line 1 type RSA
2048 f4:4b:f4:8c:c5:50:f6:c8:d3:b2:e9:14:68:86:b5:7b newremote (RSA)

If you can’t check the host key yourself, have the administrator send it to you over a secure, trusted channel, such as in person or via a PGP signed message. If the colon-delimited SSH fingerprint is not exactly the same, then you might be the victim of someone attempting to spoof your connection!

This is definitely overkill for new virtual machines and probably new machines on a trusted LAN, but for machines accessed over the public internet, it’s a very prudent practice.

Similarly, ssh(1) by default keeps a record of the host keys for hosts, which is why when a different host key is presented on a connection attempt, it warns you:

$ ssh newremote
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
d7:06:51:16:80:f6:32:b4:35:7c:53:8d:5a:49:69:ec
Please contact your system administrator.
Add correct host key in /home/tom/.ssh/known_hosts to get rid of this message.
Offending RSA key in /home/tom/.ssh/known_hosts:22
RSA host key for newremote has changed and you have requested strict checking.

Again, this is something ssh(1) users often turn off, which is pretty risky, especially if you are using password authentication and hence might send your password to a malicious or compromised server!

Restricting public keys

It may be the case that while you’re happy to allow a user or process to have public key authentication access to your server via the ~/.ssh/authorized_keys file, you don’t necessarily want to give them a full shell, or you may want to restrict them from doing things like SSH port forwarding or X11 forwarding.

One method that’s supposed to prevent users from accessing a shell is by defining their shell in /etc/passwd as /bin/false, which does indeed prevent them from logging in with the usual ssh or ssh command syntax. This isn’t a good approach because it still allows port forwarding and other SSH-enabled services.

If you want to restrict the use of logins with a public key, you can prepend option pairs to its line in the authorized_keys file. Some of the most useful options here include:

  • from="<hostname/ip>" — Prepending from="*.example.com" to the key line would only allow public-key authenticated login if the connection was coming from some host with a reverse DNS of example.com. You can also put IP addresses in here. This is particularly useful for setting up automated processes through keys with null passphrases.
  • command="<command>" — Means that once authenticated, the command specified is run, and the connection is closed. Again, this is useful in automated setups for running only a certain script on successful authentication, and nothing else.
  • no-agent-forwarding — Prevents the key user from forwarding authentication requests to an SSH agent on their client, using the -A or ForwardAgent option to ssh.
  • no-port-forwarding — Prevents the key user from forwarding ports using -L and -R.
  • no-X11-forwarding — Prevents the key user from forwarding X11 processes.
  • no-pty — Prevents the key user from being allocated a tty device at all.

So, for example, a public key that is only used to run a script called runscript on the server by the client runscript@client.example:

command="runscript",client="client.example",no-pty,no-agent-forwarding,no-port-forwarding ssh-rsa AAAAB2....19Q runscript@client.example

A public key for a user whom you were happy to allow to log in from anywhere with a full shell, but did not want to allow agent, port, or X11 forwarding:

no-agent-forwarding,no-port-forwarding,no-X11-forwarding ssh-rsa AAAAD3....19Q user@client.example

Use of these options goes a long way to making your public key authentication setup harder to exploit, and is very consistent with the principle of least privilege. To see a complete list of the options available, check out the man page for sshd.