ssh

Adding Key-Based SSH Authentication to Yocto

I recently needed to add SSH keys to a Yocto image, but realised that I have no idea how that is done. When I tried to figure out how to do it, I realised that I didn’t even fully know what I actually wanted to achieve. This text is supposed to be a quick crash course on the different keys used in SSH servers, and how to generate and use them in Yocto.

Step zero of this SSH process is adding either ssh-server-dropbear or ssh-server-openssh to your IMAGE_FEATURES to select the SSH server you want to use. After the choice is made, we are good to go and ready to start logging in.

I feel like 90% of my blog posts either talk about how to get into a system, or how to prevent people from getting into systems.

Host Key

The host key is the key that is used to identify the SSH server. When you’re connecting for the first time to an SSH server, you’ll get a warning that the identity of the server isn’t known. Once you accept the server identity, it’ll get added to known_hosts file. If the server generates a new host key every time it boots up, you’ll get a new warning that the identity doesn’t match the expected one. If a server’s identity suddenly changes, it can also mean that something malicious is going on, and the connection is usually aborted.

The host key consists of both public and private portions, and both are stored on the server. The public key is given to the client so that it can check it during subsequent connection attempts, and the private key is used to sign messages that the client can then verify with the public key. Note that the host key should be unique for each SSH server, meaning that building the key into the firmware image should be reserved for development and testing environments. For real-world deployments, you want to generate a random host key on the device to persistent storage before shipping the device into the wild. Storing the generated public key is a good idea so that you can confirm the device’s identity later on.

Generating Keys

OpenSSH

First, we need to generate the keys. This step varies a bit, depending on the SSH server you’re using. To generate keys for OpenSSH, you can run the following:

ssh-keygen -t rsa -b 4096 -f ssh_host_rsa_key
ssh-keygen -t ed25519 -f ssh_host_ed25519_key

Having multiple key types offers security, flexibility, and both backwards compatibility and future-proofing. RSA is quite old, but using it with longer keys should still be an acceptable fallback for older clients without ed25519 support. ed25519 is a more modern algorithm that’s faster and hopefully harder to crack, but not fully supported in all SSH clients. ecdsa is also a modern algorithm that can be used with OpenSSH. If you know that you’re not going to require the fallback for the older clients you can skip the RSA keys.

Dropbear

Creating keys for Dropbear is quite similar, but we need to use dropbearkey command instead:

dropbearkey -t rsa -s 4096 -f dropbear_rsa_host_key
dropbearkey -t ed25519 -f dropbear_ed25519_host_key

It’s worth noting that the OpenSSH command creates two separate files, one for the public and another one for the private key, while Dropbear only creates one file that contains both. That’s because they have their own key format.

Yocto Recipe

In Poky there already is a recipe that installs pre-generated host keys to the system: ssh-pregen-hostkeys. We can use that recipe as a basis, copy the keys to a location where the recipe can find them and write a recipe like this:

SUMMARY = "Pre-generated host keys"

SRC_URI = "file://dropbear_rsa_host_key \
           file://dropbear_ed25519_host_key \
           file://ssh_host_rsa_key \
           file://ssh_host_rsa_key.pub \
           file://ssh_host_ed25519_key \
           file://ssh_host_ed25519_key.pub"

LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

INHIBIT_DEFAULT_DEPS = "1"

do_install () {
    install -d ${D}${sysconfdir}/dropbear
    install ${WORKDIR}/dropbear_*_host_key -m 0600 ${D}${sysconfdir}/dropbear/

    install -d ${D}${sysconfdir}/ssh
    install ${WORKDIR}/ssh_host_*_key* ${D}${sysconfdir}/ssh/
    chmod 0600 ${D}${sysconfdir}/ssh/*
    chmod 0644 ${D}${sysconfdir}/ssh/*.pub
}
It’s open source, I don’t have to explain anything.

Note that you should remove the parts that do not apply to your use-case because you most likely do not want to have both Dropbear and OpenSSH keys in the system.

What if I want to generate the keys during the build?

I’d recommend creating a pre-build script that runs the key-generation commands before starting the build. The SSH packages in Yocto don’t provide native versions, meaning that the ssh-keygen and dropbearkey won’t get compiled for the build architecture. This in turn means that you cannot call these commands in recipes without host contamination. Instead of trying to call the commands in a recipe, a shell script like this could work:

ssh-keygen -t rsa -b 4096 -f ssh_host_rsa_key -N ""
ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N ""
dropbearkey -t rsa -s 4096 -f dropbear_rsa_host_key
dropbearkey -t ed25519 -f dropbear_ed25519_host_key
cp ssh_host_*_key* <RECIPE_SRC_URI_DIR>
cp dropbear_*_host_key <RECIPE_SRC_URI_DIR>

Authentication Key

The second type of key we’re usually interested in is the authentication key. This can be used for SSH login without having to enter a password. It’s also usually recommended over the password login because passwords are a quite poor method of authentication. In key-based authentication, the public key gets stored on the server (the Yocto image in our case), and the client uses the private key it has to prove it should be allowed in.

Generating Keys

OpenSSH

I recommend using ssh-keygen, because the public keys generated with it are compatible with both Dropbear and OpenSSH. The command to create the keys is the same as before. I’ll stick to ed25519 for security:

ssh-keygen -t ed25519 -f ssh_auth_ed25519_key

Dropbear

Dropbear is a bit trickier, in the sense that it requires TWO commands instead of one. First, we create the key pair:

dropbearkey -t ed25519 -f dropbear_ed25519_auth_key

Then we’ll extract the public key from the key file:

dropbearkey -y -f dropbear_ed25519_auth_key > ./dropbear_ed25519_auth_key.pub

This may create a file with multiple lines that contain some unnecessary information. You should remove all the other lines except the one that begins with ssh-ed25519.

Yocto Recipe

On the Yocto side, we want to add a generated public key to the ~/.ssh/authorized_keys file for each of the users that should be allowed to log in. Adding the following snippet to an image recipe can be used to do that:

# Add example user
inherit extrausers

# Hashed password, unhashed value is "password"
PASSWD = "\$6\$vRcGS0O8nEeug1zJ\$YnRLFm/w1y/JtgGOQRTfm57c1.QVSZfbJEHzzLUAFmwcf6N72tDQ7xlsmhEF.3JdVL9iz75DVnmmtxVnNIFvp0"

EXTRA_USERS_PARAMS:append = "\
    useradd -u 1200 -d /home/serviceuser -p '${PASSWD}' -s /bin/sh serviceuser; \
"

# Location of the authentication keys
AUTH_KEYS_DIR ??= "${TOPDIR}/../auth-keys"

# Function to install the auth keys
configure_ssh_auth_key() {
    AUTH_KEYS=$(ls ${AUTH_KEYS_DIR}/*-auth-key.pub)
    for auth_key in ${AUTH_KEYS}; do
        user=$(basename "$auth_key" | cut -d'-' -f1)
        mkdir ${IMAGE_ROOTFS}/home/${user}/.ssh
        cat ${auth_key} >> ${IMAGE_ROOTFS}/home/${user}/.ssh/authorized_keys
    done
}

# Leading ; shouldn't be required, but seems to fail without it
ROOTFS_POSTPROCESS_COMMAND:append = ";configure_ssh_auth_key;"

This approach assumes that the keys have the following naming scheme: <USER_NAME>-<ALGORITHM>-auth-key.pub. It also assumes that the keys are located in a directory named auth-keys one level above the build directory. Also, it is assumed that the user has their home directory already created when the key is copied. That’s a lot of assumptions, but assuming you follow the key file naming scheme things should work just fine. You can configure the key folder to a custom location using AUTH_KEYS_DIR variable.

The example above adds a new user named serviceuser to the system for demonstration purposes. So, if the AUTH_KEYS_DIR points to a directory that contains serviceuser-ed25519-auth-key.pub, the key should get copied to the user’s authorized_keys and it should be possible to log in as them using SSH. If you want to read more about adding users to your Yocto images, you can check out my other blog text.

The first thought I had was using SRC_URI and installing the files from WORKDIR, but that doesn’t work because image.bbclass defines do_fetch as noexec. According to this StackOverflow answer, you can define those as executable with Python function, make bitbake fetch the certificates from SRC_URI, and install them in a custom task, but it’s a bit risky to edit the core classes like that, so I’d advise against it.

Also, once again, if you want to generate the keys during the build, I recommend using a pre-build script to create and store the keys.

Connecting

The next logical step is of course connecting. I’m going to assume you have a service that starts the SSH server on the image, such service should exist by default if you install an SSH server using IMAGE_FEATURES. Note that OpenSSH and Dropbear clients expect the private key in their specific format, so you can’t use OpenSSH private key on Dropbear client or vice versa. Unless you convert them, more on that a bit later.

One thing to note when attempting an SSH connection. If you get asked for a password even when you shouldn’t, there’s a high chance that the user you’re trying to log in as has been locked. This happens for example if you don’t define a password when creating a user. Check the system log for relevant messages to debug the issue if you encounter it.

OpenSSH

OpenSSH client is the classic, fan-favourite ssh. To connect to a remote server with your private key, simply run:

ssh -i <PRIVATE_KEY_FILE> <USERNAME>@<IP>

The magic is the -i flag. You may want to add the key to the ssh-agent so that you won’t have to use the flag every time:

# Start ssh-agent
eval "$(ssh-agent -s)"
# Add key
ssh-add <PRIVATE_KEY_FILE>

After that the ssh command “should just work”. I personally don’t like things that “should just work”, but it saves some typing if you’re constantly connecting to a server.

Dropbear

Honestly, I didn’t even know that Dropbear has its own SSH client before writing this text. It turns out it has, which makes sense considering that it has its own key format. The client is called dbclient, and as one can guess, it focuses on being lightweight. One thing that may not be obvious from its name is that it has nothing to do with databases though. To connect to a remote server with the Dropbear format key, use the following command:

dbclient -i <PRIVATE_KEY_FILE> <USERNAME>@<IP>

Converting Keys

If you end up in a situation where you have the wrong type of private keys for your SSH client, you can use dropbearconvert to convert a key from Dropbear format into OpenSSH format:

dropbearconvert dropbear openssh <SOURCE_KEY> <DESTINATION_KEY>

The conversion from OpenSSH to Dropbear isn’t much harder either:

dropbearconvert openssh dropbear <SOURCE_KEY> <DESTINATION_KEY>

It’s worth noting that dropbearconvert converts only private keys. That’s because both OpenSSH and Dropbear use a similar type of public keys.

That’s all for this topic. These instructions should help you build an image that has a constant host certificate, and that you can connect to easily and password-free. Both are quite basic yet important features for a firmware image. If you have questions or comments let me know in the comments.