This is a guide to setting up a YubiKey for use as an offline SSH certificate authority.

This assumes a brand new YubiKey with no prior configuration on it, to be used solely as a CA.

Why?

Typically a CA should be on a secured, isolated machine. Using a dedicated YubiKey means you can isolate your CA and keep it in a drawer so that it can’t be accessed. YubiKeys offer protections such as requiring a PIN and/or touching the key for PIV operations.

Using an SSH CA might sound like overkill for personal use, but it provides a lot of conveniences: you only need to configure remote machines once, with the CA public key. Any new key pairs you create don’t need distributing to those machines. As long as the client presents a certificate signed by the CA, the remote machine will accept any new keys. This removes the need for manually copying public keys or adding them to configuration management.

It also provides automatic verification of your servers: you don’t need to accept the server’s public key and add it to the known_hosts file. As with the client keys, as long as the server presents a certificate signed by your CA, you can automatically verify it.

Prerequisites

  • YubiKey Manager (aka ykman)
  • A password manager or other tool to generate random passwords and store your CA management key, PIN and PUK
  • A PKCS#11 library for OpenSSH, one of:

I’ll assume the use of libykcs11.so as I found it more straightforward to use. OpenSC works, but depending how it’s installed it might not work out of the box with SSH (looking at you, Homebrew), which requires libraries to be in a location that’s hard-coded at compile-time.

(If you have multiple YubiKeys) Identify the YubiKey to use

List all connected YubiKeys

$ ykman list
YubiKey 5 NFC [OTP+FIDO+CCID] Serial: xxxxx
YubiKey 5 NFC [OTP+FIDO+CCID] Serial: yyyyyyyy

The first one is used as my current SSH key. The second is the new one to set up as the CA. We can check this by looking at the PIV application on each.

$ ykman --device yyyyyyyy piv info
PIV version: 5.2.6
PIN tries remaining: 3
CHUID:	No data available.
CCC: 	No data available.

$ ykman --device xxxxx piv info
PIV version: 5.1.2
PIN tries remaining: 3
CHUID:	No data available.
CCC: 	No data available.
Slot 9a:
	Algorithm:	ECCP256
	Subject DN:	CN=SSH key
	Issuer DN:	O=yubikey-agent,OU=v0.1.1
	Serial:		xxxxx
	Fingerprint:	abcdef
	Not before:	2020-05-10 11:50:33
	Not after:	2062-05-10 12:50:33

We can see the second one has PIV configured by yubikey-agent and the first has nothing configured in any slots.

Setup

Let’s set up the new YubiKey by changing the management key, PIN and PUK.

Info Below I use --device in every ykman call as I had two keys plugged in. If you only have your CA key plugged in you can omit this.

Here we change the management key (hit Enter at the first prompt to use the default key) and tell ykman to generate a random key (--generate), and require touching the YubiKey whenever the management key is used (--touch).

Be sure to save the generated management key somewhere safe! You will need it any time you want to sign.

$ ykman --device yyyyyyyy piv access change-management-key --touch --generate
Enter your current management key [blank to use default key]:
Generated management key: <some string of characters>

123456 is the default PIN and 12345678 is the default PUK. You can use pwgen or a password manager to generate a new PIN and PUK. See Yubico’s PIN and Management Key documentation for PIN limitations.

$ pwgen -sy 8 1
$ ykman --device yyyyyyyy piv access change-pin -P 123456
Enter your new PIN:
Repeat for confirmation:
New PIN set.
$ ykman --device yyyyyyyy piv access change-puk -p 12345678
Enter your new PUK:
Repeat for confirmation:
New PUK set.

We only need CCID mode on the CA YubiKey, OTP and FIDO won’t be used, so let’s disable them.

$ ykman --device yyyyyyyy mode CCID
Set mode of YubiKey to CCID? [y/N]: y
Info I had to reinsert the YubiKey after doing this, otherwise ykman generated a traceback during the next step with error ykman.driver_ccid.CCIDError: Failed to transmit with protocol T1. Card was reset.

Creating the CA

First we create a private key. We’ll use slot 9c.

The ca-key.pub argument tells ykman to write the public key for the generated key to the file ca-key.pub. This will be used when we generate the certificate in the next step.

$ ykman --device yyyyyyyy piv keys generate 9c ca-key.pem
Enter a management key [blank to use default key]:
Touch your YubiKey...

Now generate a certificate whose subject is “SSH CA” and is valid for ~10 years, using the public key from the last step.

$ ykman --device yyyyyyyy piv certificates generate -s "SSH CA" -d 3650 9c ca-key.pem
Enter PIN:
Enter a management key [blank to use default key]:
Touch your YubiKey...

At this point we have a private key and self-signed certificate on the YubiKey.

$ ykman --device yyyyyyyy piv info
PIV version: 5.2.6
PIN tries remaining: 3
CHUID:	aaaa
CCC: 	bbbb
Slot 9d:
	Algorithm:	RSA2048
	Subject DN:	CN=SSH CA
	Issuer DN:	CN=SSH CA
	Serial:		abcde
	Fingerprint:	abcde
	Not before:	2020-10-04 15:21:26
	Not after:	2030-10-02 15:21:26

Let’s get the SSH public key for this. You’ll need the path to the shared library for the PKCS#11 library as mentioned in the prerequisites.

ssh-keygen -D /opt/local/lib/libykcs11.dylib
Info If you have multiple YubiKeys plugged in this will be problematic and might return data for the wrong key.

This will output two or three ssh-rsa keys. Take the second one with comment “Public key for Digital Signature” and put it in a file called ca.pub. This is the signing key used by ssh-keygen to sign keys, and by sshd and ssh to validate certificates.

Signing SSH public keys

The part you’ve probably been waiting for! This section is what you’ll do any time you create a new SSH key pair.

I keep all public keys and certificates stored on the computer I primarily use my CA key with, mostly for convenience. I structure it as:

/Users/jamesog/ssh-ca
├── host
└── user

I copy all host keys into ~/ssh-ca/host/ and user keys to ~/ssh-ca/user/. The signed certificates then live alongside the public key.

When signing keys you have to provide a “key ID”. For user keys, this is what’s logged on the server when you authenticate with a certificate. I use my email address for the ID.

Info The examples below generate certificates with no validity interval, meaning they are valid forever. If this doesn’t match your security standards, or you only want to grant limited access, use the -V flag to ssh-keygen. See ssh-keygen(1) for information on how to use that.

Host keys

The certificate needs an ID (-I), which is usually the machine’s hostname, and a principal (-n) of the machine’s hostname (usually a DNS name).

ssh-keygen -s ca.pub -D /opt/local/lib/libykcs11.dylib -I <hostname> -h -n <hostname> host/somehost_ed_25519.pub

If the machine can be accessed using several hostnames, add each of them to the -n flag separated by commas. If you ever need to access the machine by IP address instead of a hostname, add that to the principals too, otherwise it won’t verify.

User keys

The certificate needs an ID (-I) which should be something to identify you — I use my email address — and your remote username as a principal (-n).

$ ssh-keygen -s ca.pub -D /opt/local/lib/libykcs11.dylib -I <email address> -n jamesog user/id_ed25519.pub
Enter PIN for 'YubiKey PIV #yyyyyyyy':
Signed user key user/id_ed25519-cert.pub: id "<email address>" serial 0 valid forever

You can inspect the generated certificate with ssh-keygen:

$ ssh-keygen -L -f user/id_ed25519-cert.pub
user/id_ed25519-cert.pub:
        Type: ssh-ed25519-cert-v01@openssh.com user certificate
        Public key: ED25519-CERT SHA256:xxx
        Signing CA: RSA SHA256:xxx (using rsa-sha2-512)
        Key ID: "<email address>"
        Serial: 0
        Valid: forever
        Principals:
                jamesog
        Critical Options: (none)
        Extensions:
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc

Signing another YubiKey

If you use another YubiKey SSH private key (for example using yubikey-agent), you can also sign the public key for this.

Info Make sure you unplug the CA key first, otherwise you’ll get both keys’ public keys in the output.

Export the SSH public key from the YubiKey:

ssh-keygen -D /opt/local/lib/libykcs11.dylib > user/yubikey.pub

And then sign the public key as above.

Setting up servers

Presenting the signed host certificate

Copy the signed host certificate to each server, e.g. host/somehost-cert.pub, and place it alongside the host’s private keys, usually in /etc/ssh, ensuring the name matches the private key name. Add the following directive to sshd_config:

HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub

Restart sshd after adding this directive (or wait until you’ve configured client keys, below).

Authenticating client certificates

sshd can be configured with the CA certificate so that any user presenting a certificate signed by this CA can be authenticated with no other configuration, assuming the certificate has a valid principal for the user they’re trying to authenticate as.

Take the ca.pub from earlier and install it on each server, e.g. at /etc/ssh/ca.pub and add the following directive to sshd_config:

TrustedUserCAKeys /etc/ssh/ca.pub

Restart sshd after adding this directive. See sshd_config(5) for more information, and also the AuthorizedPrincipalsFile in case you want to do more advanced things with user principals.

Alternatively, each user can add the CA to their ~/.ssh/authorized_keys file:

cert-authority,principals="jamesog" ssh-rsa ...

The principals= here ensures only certificates with the correct usernames can authenticate.

Setting up clients to use the certificate

In your ~/.ssh/config use the CertificateFile directive to use the certificate. If your key is loaded in ssh-agent, ssh will match the certificate to the private key, otherwise add IdentityFile to your SSH config too.

If your private key is not a file on disk (i.e. you’re using a YubiKey or a Mac’s Secure Enclave) you should set the IdentityFile directive to be the path to your public key. It’s nonsensical, I know, but somehow it works.

Setting up clients to authenticate server host keys

Add to ~/.ssh/known_hosts the DNS name(s) or a wildcard of remote servers that have the signed certificate, followed by the contents of ca.pub:

@cert-authority *.example.com ssh-rsa ... SSH CA

With this you no longer need TOFU or to accept individual host keys.

Further reading

Secure hardware-backed private keys

Using a regular key pair generated with ssh-keygen is just fine, however for added security you can make the private key completely inaccessible by using a hardware device.

YubiKey

As we’ve already discused, the YubiKey is great for this. yubikey-agent makes setting up the key and your computer very straightforward.

The bonus of a YubiKey is it is transportable and can be used across computers.

Mac Secure Enclave

Every modern (since 2016) Mac comes with a built-in security device called a Secure Enclave. This is where things like your Touch ID fingerprints are safely stored.

Secretive uses the Secure Enclave to generate and store SSH keys. The private key can never be extracted so you can’t transfer the key to aonther Mac.

Secretive has a couple of great usability features: you can get notifications whenever the key is used, in case some nefarious process is using your key in the background, and you can require biometrics (Touch ID or Apple Watch) every time the key is used for maximum security. This is similar to requiring a tap of the YubiKey when the key is used, but only you can authenticate it.