SSH from a bird's eye view, or sorting out a pile of keys
Published: 2023-08-06 07:07:05

Recently I’ve decided to learn more about the SSH protocol. I had regularly used it to run shell commands remotely, but only vaguely understood, why the admins always ask to send them some kind of keys, what happens to this key during the connection, why `ssh` periodically yells weird warnings at me, and so on. Surprisingly, I didn’t find any resources that would explain all of that so that I don’t have more questions after reading than I started with. So, after reading specifications and playing around with OpenSSH, I would like to describe it here.

This article is targeted at those, who are superficially familiar with SSH. Maybe you used it in practice, but didn’t grasp all of its magic. I will describe the principal security aspects of the protocol: how many and which keys and algorithms are used and why. Also, I will give a brief overview of how some of the features are implemented in OpenSSH on Linux (which is one of the existing implementations of the protocol).

### What you need to know

The only requirement is to know, how (a)symmetric encryption and digital signatures work. I will not talk about what is a cryptographic key, what is the difference between a public and a private key and what they do. There are plenty other sources on that matter.

### Terminology

From now on

- A "user" is a system user, the one you create with the `adduser` command on Linux or in system preferences on Windows.
- A "client" is a program implementing the client side of the SSH protocol. It sends request to the SSH server, writes wrathful tirades into your terminal window because some key changed, etc. Many Linux distributions have a preinstalled OpenSSH client which can be run using the `ssh` command.
- A "server" is therefore a program implementing the server side of SSH. Many Linux distributions have it present in the form of the `sshd` daemon. We won’t be talking about how to set it up.
- An "endpoint" is the client or the server.
- A "peer" is the endpoint remote to the currently discussed one.
- "You" is you, the person who is using the client.
- An "admin" is a person in charge of the server. Possibly coincides with "you". It can also be some kind of an automatic system, for example, if you are using cloud services. In that case you interact with it through your cloud platform control panel.

## What is SSH?

> The Secure Shell Protocol (SSH) is a cryptographic network protocol for operating network services securely over an unsecured network. Its most notable applications are remote login and command-line execution.

This is the first paragraph of the Wikipedia article on SSH. You probably know this if you did any amount of research on the topic.

The less trivial part (written in the second paragraph) is that SSH really consists of three more or less independent protocols: transport layer protocol, authentication protocol, and connection protocol. It is possibly too much of an honor to call theese “protocols”, one could say these are the three stages of a single SSH protocol. But this is what they are called in the specification, so I will call them that as well. By the way, every one of them has a separate specification: [RFC 4253](https://www.rfc-editor.org/rfc/rfc4253), [RFC 4252](https://www.rfc-editor.org/rfc/rfc4252), and [RFC 4254](https://www.rfc-editor.org/rfc/rfc4254) respectively. This is where you should first search the answers to any questions regarding SSH.

In practice, usually, this separation does not come up very often. However, it is quite important to understanding the protocol, so we’ll go over each of the protocols separately.

## Act 1. Transport layer protocol
### Scene 1. Establishing a secure connection

The first step that SSH performs is establishing a secure link. Spoiler: later we will send a bunch of confidential data (for example, your passwords) over this connection. Therefore we need some jumping around so that no one malicious can read these data. This is, in my opinion, the most complex part of the protocol, because there is a lot of security considerations.

So, the first thing the endpoints do after an insecure network connection is established between the devices (SSH usually runs over TCP, so this is after a TCP connection is established) is sending each other their software version and the version of SSH they will to use (at the moment of writing the latest version is 2.0). Next they exchange lists of supported algorithms that will be used thereafter.

The SSH developers anticipated that, on the one hand, cryptographical algorithms, like everything in our lives, get out of fashion over time (that is, people discover vulnerabilities in them). On the other hand, maintainers of SSH client and server software are not able to support all existing algorithms. This is why SSH has a step where they are negotiated. So how many algorithms do we need and why? Not one, and not even two.

The first one is the **compression algorithm**, which is the most boring. it is simply used to compress every transmitted message to save bandwidth.

> Remark 1: firstly, there may be two compression algorithms: one for message from the client to the server, and another the other way. For compression this is not too useful, however such possibility exists for other algorithms where it might be more appropriate.
>
> Secondly, I call the compression algorithms “the first one” only because it appears first in the article. In reality, endpoints negotiate all algorithms at once. Indeed, the transport layer protocol handshake completes in at most 4 round-trips (so at most 8 messages exchanged). The fact this article is quite long shouldn’t lead you to believe that the protocol is as well. The amount of messages that need to be exchanged is heavily optimized.

In SSH the words “secure connection” mean that, among other things, every message is encrypted using symmetric encryption. That is, both endpoints have (usually) the same symmetric encryption key (we’ll further refer to them simply as **encryption key**), which can both encrypt and decrypt any message. There are all kinds of symmetric encryption algorithms (or simply **encryption algorithms**), so this is the second thing that gets negotiated

> Remark 2: as with compression, there may be two encryption algorithms chosen. Although the specification recommends that the same one is used both ways.

We have a problem: how is it possible that the endpoints agreed on the same encryption key using an insecure connection, and no one else found out what the key is? The answer is they used a **key exchange algorithm** (or “kex algorithm”). This is the third one that gets negotiated.

Specific kex algorithms are variations of the Diffie-Hellman key exchange algorithm. Wikipedia has a comprehensive picture explaining it (see below) where colors are used instead of numerical keys. Firsly, endpoints agree on some non-secret value (usually the client generates it and sends to the server over the insecure connection. In the picture this public value is the yellow color). Next, both endpoints generate a private secret (in the picture I shall interpret those as the turquoise and the sunset colors). It is somehow combined with the public value (in the picture — the colors are mixed. More generally the way this is done depends on the chosen kex algorithm) and the result is sent to the peer.

Then the endpoints combine their private secret and the result received from the peer in some way, and the magic of math guarantees that they both get the same result. This result, which is called the common secret, can now be used to generate the encryption key. In practice it is produced as a hash of the common secret and some other data that both endpoints are aware of. The hash function used is bound to the negotiated kex algorithm, so it is the same one for either endpoint.

![Illustration of Diffie-Hellman key exchange](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b8c58rfvbidghst60foz.jpeg)

However, symmetric encryption is not sufficient for the connection to be called “secure”. We also need the now fourth **MAC algorithm** which needs the **integrity key** (MAC stands for Message Authentication Code). It is as basic and important as the encryption algorithm, and both integrity checks and encryption happen under the hood of the SSH implementations. You, as a developer, hardly ever need to worry about these.

MAC solves the following problem: encryption guarantees, that if there is an intruder in your connection, they cannot understand what the content of your message is. However, nothing prevents them from modifying it. Yes, the intruder probably won’t be able to, for example, run a specific command, because he doesn’t know the encryption key and so cannot properly encrypt it. But he certainly can add gibberish. Best case scenario: the command you sent to the server won’t run because of a syntax error, worst case: better not think about it.

This is what MAC protects for: it is a small sequence of bytes that is computed from the contents of your message and the secret integrity key, and appended to the end of the message. Using that same key any endpoint can verify, that the message was no modified in transit. The integrity key is also generated as a hash of the common secret + some data that both server and client know (refer to the RFC to learn precisely which data).

These three algorithms (kex, encryption, and MAC) are sufficient to establish a secure connection: every message is encrypted on sending and decrypted on receipt, the MAC serves to ensure it is not modified in transit, and the kex algorithm is used to generated shared secret keys.

But there is another important action that is taken during the key exchange: the client authenticates the server. We are used to thinking that the client needs authentication so that no intruder can connect to the server and run malicious code. However, in SSH server authentication is equally important: the client is going to send its confidential data, so it needs to make sure the server is who it pretends to be. Everything described above doesn’t guarantee that: a client could as easily have performed the key exchange not with the real server but with a malicious one.

This is where the next algorithm comes in: the host digital signature algorithm (or **host key algorithm** for short).

### Scene 2. Server authentication

Above with discussed the basic security measures that all happen behind the curtains: you as a developer pretty much never see any of the algorithms or keys discussed. Now we shall talk about something more practical that you will encounter when working with SSH.

The server initially has a pair key: a public and a private, these are called the **host keys**. In fact, there not a single pair like this: there is one for each host key algorithm the server supports. OpenSSH on Linux stores these in the `/etc/ssh` folder. Every private key is stored in a file named `ssh_host_*_key` and public one in `ssh_host_*_key.pub`, where the asterisk is replaced with the name of the algorithm the key belongs to. For instance, `ssh_host_rsa_key` or `ssh_host_ecdsa_key`. It is the admin who puts these keys there before launching the server. For example, they can generate brand new keys using the `ssh-keygen -A` tool, or maybe they just happen to have them lying somewhere in their pocket, who knows.

During the key exchange the server signs some particular string using its private host key (corresponding to the chosen algorithm) and sends both the signature and the public key to the client. This string, again, consists of some data that the client is also aware of (the RFC specifies which data exactly), so it is able to construct the same string and verify the signature, therefore ensuring that the server indeed possesses the corresponding private key. The string could be sent to the client alongside the signature and the key, but presumably this is not done to save a little bit of bandwidth.

However, this doesn’t mean anything yet. A malicious person could as simply generate a pair of keys, sign a string with their key and send the signature to the client. This signature verification only ensures that the server possesses the private key corresponding the advertised public one, but not that the server is legitimate.

Ensuring this is not as trivial as applying some algorithm. There are different options, some more secure, some less. The most common one is as such: the client stores a small database which associates each server that you connect to to its public key. OpenSSH on Linux implements this database as a simple file `~/.ssh/known_hosts` where you must specify the server domain and the corresponding public key.

Let’s consider this scenario: I need to SSH to a remote machine that is located at `example.com`. To do that, being a responsible person, I go to the administrator of this server and ask them for the public key (corresponding to the host key algorithm I am willing to use) this server has. Next, I add this key to the `known_hosts` file and associate it with the example.com domain (you can find the precise format of this file on the Internet). Now every time I connect to `example.com` using SSH, the client will verify that the public key the server sent matches the one specified in `known_hosts`. If it does, the client will verify the digital signature. If it matches, this means that the server has the corresponding private key, therefore it is legit. A little remark: if you want to connect to that same server using not only the domain name, but the IP address as well, you will need to associate the public key with both of these separately, because SSH client doesn’t know which address belongs to which domain.

However, if you used SSH before, you probably never modified `known_hosts` manually. This is because when you connect to a new server, OpenSSH will show you this message:

```
The authenticity of host '...' can't be established.
ED25519 key fingerprint is SHA256:noP2S4gaQmKTIO8cHHg4ju3QptLSo6MEgCw2AiLwOJM.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?
```

This way the client tells you it doesn’t know if the server is legitimate and lets you decide. You have three options:

- Type `no`, and abort the connection.
- Type `yes`. Then the client will automatically put the public key the server sent into `known_hosts`, so that it can authenticate the server on subsequent connection attempts. Before thoughtlessly doing it, you can pay attention at the line above, where OpenSSH prints the SHA256 hash of the server’s public key. You can compare it with the expected one, for example ask the administrator if this is the right hash. If you are using cloud services, the hash may be specified somewhere in the control panel.
- Type a fingerprint. This is, roughly speaking, a hash of the public key which you may also get from the admin or your server control panel. In this case the client will automatically verify that the public key matches the fingerprint you provided, and add the key to known_hosts if it does.

The important point is that server’s public key is used for authentication and saved on the client. Therefore, if the key changes, authentication will fail. The key can change for legitimate reasons, for example the admin reinstalled the SSH server or the operating system, or maybe the keys were changed because the old ones had been compromised. But the client cannot distinguish these legitimate scenarios between someone trying to compromise your connection. In such cases OpenSSH will yell at you like this:

```
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    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
6e:45:f9:a8:af:38:3d:a1:a5:c7:76:1d:02:f8:77:00.
Please contact your system administrator.
Add correct host key in /home/hostname /.ssh/known_hosts to get rid of this message.
```

This message speaks for itself: server’s public key changed, so someone might be doing something malicious, or everything might be fine. You can only know for sure if you contact the admin. There are ways to implement regular key renewal, but I am not going to talk about them. Refer to “SSH key rotation” if you are interested.

Storing the server’s public key in a database is not the only way to authenticate the server. You can also use certificates or hardware tokens or whatever. But this is the most basic and ubiquitous one.

### Summary

After all these steps we have an ability to securely exchange messages over an insecure connection, and some confidence that the server is legitimate. To do that we used

- A pair of host keys: the server has the private one, and both endpoints has public ones. These are used to verify server’s identity.
- Encryption key and integrity key. These are common to both endpoints, they are only stored in memory and not saved for later use. Generally speaking, these are ephemeral: they can change during the connection (this is accomplished with the key reexchange procedure). But these are minor details. You as a developer don’t see these keys, they are used under the hood.

Notice, that we never talked about client keys yet. The id_rsa and id_rsa.pub keys that you might have generated and sent to someone will come up later.

## Act 2. (Client) Authentication protocol

Before now, server did a bunch of things to prove its identity to the client. The time has come for the client to finally prove something as well. The procedure above could be easily accomplished by a malicious client, so the server needs to make sure the user has permission to connect.

The easiest way to authenticate the client is **not authenticating the client**. One can set the server up for certain users in a way where after the secure connection is established, you are automatically logged into that user. Which user exactly you want to log in as, you specify when connecting, for example ssh myuser@example.com. If authentication is disabled for myuser then you will neither need any passwords, nor keys. Of course, it is very insecure, but sometimes it maybe reasonable to do so.

But for us it is unreasonable. We now have a secure connection, so we can send confidential information without worrying anyone is eavesdropping. So the less easiest way to authenticate the client is **using a password**. The client simply sends the username and the password (that someone set up earlier) when connecting, the server verifies they match, and if they do — boom, you are connected. Usually the username and password are just system user’s username and password that you want to log in as. The password authentication method is not obligatory: not all servers support it.

It is however obligatory to support the even less easiest option: **using a client key**. In this scenario you manually create a pair of client keys (for example, using the `ssh-keygen` utility which puts the private key into `~/.ssh/id_rsa` and the public one into `~/.ssh/id_rsa.pub`). Then you send the public key (just the public one!) to the administrator. If the server is running OpenSSH on Linux, then every system user has a file `~/.ssh/authorized_keys` which is a list of all client keys, whose owners are allowed to log in as said user. You can add the same key to the `authorized_keys` of multiple users: then the client will be able to log in as any of those. The specific user is selected when connecting (for example `ssh myuser@example.com`).

In this case the process is similar to server authentication. The client generates some string, signs it with its private key, and sends the signature alongside with its public key to the server. The server first checks if the public key is contained in `authorized_keys` of the user that the client is trying to log in as, then constructs that same string and verifies the signature. If the signature is valid, it means that the client possesses the corresponding private key, therefore it is legitimate, and the server can start accepting commands/files/whatever.

Note that **you must never send your private key (the `id_rsa` one without the `.pub` extension) to anyone**. Sending your private key to someone is the same thing as telling your password: they will be able to log in as you anywhere you use that key. If you want to give someone access to your user account on some remote machine, better ask them for their private key and add it to authorized_keys of said user manually.

## Act 3. Connection protocol

At this point the secure connection is established, the client verified that the server is not malicious and vice versa. The connection protocol is the boring one. It works at the application level: it describes how to send commands that you want to run, send file securely, or do whatever else it is that SSH can do. So I’ll not go in too much detail here.

## Conclusion

During the SSH connection the following keys are used:

- Host key. It is a digital signature key that the client uses to verify the server. It is not used for encryption.
- Client key. It is a digital signature key that the server uses to authenticate the client. It is not used for encryption. It is only necessary if key authentication method is used. If you use a password, client key is not necessary.
- Encryption key and integrity key. They are new for every connection, they can change during the connection, and are used to make it secure.

If you are using OpenSSH on Linux, then here is the list of files that are used. On the client:

- `~/.ssh/id_rsa` — client’s private key, you must never send it to anyone.
- `~/.ssh/id_rsa.pub` — client’s public key, that you send to the administrator to get access to the server (if key authentication method is used).
- `~/.ssh/known_hosts` — a list that matches servers’ domain names/IP addresses to their public host keys.
- `~/.ssh/config`, `/etc/ssh/ssh_config` — OpenSSH client configuration. This was not mentioned in the article, but you can find which configuration options are available with man ssh_config.

On the server:

- `/etc/ssh/ssh_host_*_key` — server’s private host keys, one key for each supported host key algorithm.
- `/etc/ssh/ssh_host_*_key.pub` — corresponding public keys.
- `/home/<username>/.ssh/authorized_keys` — a list of public keys that belong to clients who are allowed to log in as <username>.
- `/etc/ssh/sshd_config` — server daemon configuration, see man sshd_config.

Of course, I skipped all kinds of details about the protocol, and only described the basic functionality it provides. But, hopefully this article is useful to the newbie fishies in the ocean of network protocols.