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.
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 internal development and testing environments. It is a bit of a special situation to build them into the image. If you are unsure whether you want to do this or not, you most likely just want to disable the strict key checking for SSH command. The following ssh
command connects to a remote server even if the host key is not the expected one (but use this command with caution as well!):
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no <USER>@<IP>
For real-world deployments, you want to generate a random host key to the device’s persistent storage before shipping it 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 may offer security, flexibility, and both backwards compatibility (if it is required) 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. However, if you know that you won’t need the RSA
keys as a fallback it is better just to skip them for security. 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.
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
}
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.