SSH Certificate Authority with TPM Backed Keys on Debian
Recently, I learned of the advantages of SSH certificates. I started expanding my homelab and got tired of dealing with this on new hosts:
- Having to add all of my client keys to
authorized_keys - Trust-On-First-Use warnings when I connect
I got my hands on an old Dell Optiplex Micro from work which has a TPM 2.0 device, and wanted to experiment with setting up a root CA with hardware backed, non-exportable private keys.
On my unit, I had to upgrade the TPM firmware to get the OS to recognize it.
The following steps are performed on Debian 12 (Bookworm) with a newly cleared TPM.
Prerequisites
First, we install some required packages:
sudo apt install libtpm2-pkcs11-tools libtpm2-pkcs11-1 tpm2-abrmd openssh-clientWe start the tpm2-abrmd service to allow access to the TPM from user space:
sudo systemctl enable --now tpm2-abrmdRun the following command to add the current user to the tss group to allow access to /dev/tpmrm0
sudo usermod -a -G tss "$(id -nu)"From this point on, we shouldn’t need to use sudo, and our PKCS#11 store will be bound to the current user.
We can test that the TPM2.0 device is working and accessible with this command:
tpm2_getrandom --hex 16Initializing a PKCS#11 Store
As a best practice, we will create two CAs - one for signing host keys, and one for user keys.
Initialize the first store with this command:
tpm2_ptool initIt should return the ID of the created slot, initially 1. We will use this for a host CA key. Substitute the PINs below with your own securely generated PINs, and create the token:
tpm2_ptool addtoken --pid=1 --sopin=[PIN] --userpin=[PIN] --label=host_caThis returns no output when successful.
Creating a key pair
Next we will want to create the key. First, check the available algorithms with this command:
tpm2_ptool addkey --helpecc256 is generally recommended, and we can test that it is available to us by using:
tpm2_testparms ecc256Success is indicated by no output and an exit code of 0.
Some guides suggest rsa4096, which is not possible on my TPM:
$ tpm2_testparms rsa4096
ERROR: Unsupported algorithm specificationWe will now use the addkey subcommand. --label must follow the token label we chose earlier, as well as the --userpin.
tpm2_ptool addkey --algorithm=ecc256 --label=host_ca --key-label=host_ca_key --userpin=[PIN]It should return some output showing that the key was added.
Next we will need to create a new PKCS#11 key store with tpm2_ptool init and create a new token and key similar to before, but this time for our user CA.
tpm2_ptool init
tpm2_ptool addtoken --pid=2 --sopin=[PIN] --userpin=[PIN] --label=user_ca
tpm2_ptool addkey --algorithm=ecc256 --label=user_ca --key-label=user_ca_key --userpin=[PIN]Now we can view both public keys with this command1:
ssh-keygen -D /usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so.1Let’s split these out into two files, one for each CA:
ssh-keygen -D /usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so.1 | { read first; read second; echo "$first" > host_ca_key.pub; echo "$second" > user_ca_key.pub; }With the host CA public key, we will want to add this to ~/.ssh/known_hosts on all of our clients.
The following article is a good resource on understanding SSH CAs: https://goteleport.com/blog/how-to-configure-ssh-certificate-based-authentication/
Prepend with @cert-authority *.example.com where *.example.com is a pattern match of your choosing. It can also be a comma separated list of values. In my homelab, everything uses the .lan subdomain, so a simple known_hosts starting with @cert-authority *.lan ecdsa-sha2-nistp256 is all I need to start using certificates.
Signing Certificates
Now let’s sign a host key using our CA key which is stored in a PKCS#11 token. We’ll use the following example command:
ssh-keygen -s host_ca_key.pub \
-D /usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so.1 \
-I host.example.com \
-h \
-n host.example.com \
-V +52w
ssh_host_rsa_key.pubLet’s step through the flags:
-s host_ca_key.pubwhen used in conjunction with-Dit identifies the CA key by it’s public component, which we exported earlier-D /usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so.1this is the same flag as earlier to load the PKCS#11 library-Ithis is the key identity-hdenotes a host key-nspecifies the host principals allowed to use this key - it accepts one or more hostnames or IP addresses-Vthe validity interval, which defaults to an indefinite period when not specified
The last argument specified the public key file for which we are signing. You may also have more than one for a host, such as ed25519 in addition to rsa.
This should prompt you for the user PIN set earlier. Once finished, it will result in a -cert.pub file. This can be placed in /etc/ssh on the host of choice, with the following added to your/etc/ssh/sshd_config file:
HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pubAdditionally, we need to specify the user CA public key to trust, copied from our root CA:
TrustedUserCAKeys /etc/ssh/user_ca_key.pubRestart your sshd daemon and the host certificate should be loaded.
Once you have configured your SSH servers to use their certificate signed host key, you can remove their previous entries in known_hosts from the client side.
Next, we will sign a user key in a similar way
ssh-keygen -s user_ca_key.pub \
-D /usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so.1 \
-I localuser@client.example.com \
-n remoteuser \
-V +52w
id_rsa.pub-n Should be specified as the user principal(s) you expect to connect as on the remote server(s).
The -cert.pub file should be placed alongside your key in ~/.ssh to be picked up automatically by openssh-client.
Closing Remarks
If you got this far, you’re finished! Connect to a host and never be prompted to accept a new host key again.
It’s a bit cumbersome to get this going at the start - you have to move your host and user public keys over to the CA to sign them, then distribute the certificates back. I’ve written an Ansible playbook to help automate the process that I plan to publish later.
One omission from this guide is the use of intermediate CAs. In an enterprise or production environment, this would be recommended. However, I only have one physical device with a TPM, and I decided to dedicate it to being a single root CA. My environment was too small to justify intermediate CAs, and if they could not use TPMs themselves, I deemed it less secure.
Similar to regular root CAs, I keep mine powered off and only power it on when needed. You can increase the security by using local disk encryption, isolating it on a VLAN, etc. You should also keep your userpin and sopin in a secure location.
For a more robust solution, smallstep offers an SSH CA product with a self-hostable free version. But again, with my small footprint, I stuck to the basics.
References
- Protecting Authentication Keys With a TPM 2.0
- tpm2-pkcs11 Documentation
- TPM2.0 protected SSH keys cheat-sheet
- ssh-keygen Documentation
- How to Generate and Configure SSH Certificate-Based Authentication
-
The library location may be different on non-Debian distros ↩︎