October 2026 – wow how time flies. I’d forgot about this post. As is always the case, dealing with certain vendors’ equipment may still require you to support the three common ssh key formats. Storing them securely on disk is still critical for keys you want on disk. However, an even better way to handle critical keys is to generate them in hardware tokens like Yubikeys, and set relevant access restrictions on them. No one will ever come to possess your private key since it can’t be extracted, and you protect your hardware token with PIN/PUK.
On-Disk Storage
Modern computers are getting pretty fast these days, 96 rounds in 2016 to 192 rounds in 2019 and now even 300 rounds in 2026 only takes about a second on my not particularly fast Mac Mini. At 1000 rounds I’m at 5.6 seconds, so that’s my current recommendation.
Optimal generation of the three common key types:
ssh-keygen -t ed25519 -a 1000 -C "20261017-ed25519-file" -f 20261017-ed25519
## Enter pass phrase ##
ssh-keygen -t ecdsa -b 256 -a 1000 -C "20261017-ecdsa-file" -f 20261017-ecdsa
## Enter pass phrase ##
ssh-keygen -t rsa -b 4096 -a 1000 -C "20261017-rsa4096-file" -f 20261017-rsa4096
## Enter pass phrase ##
chmod 600 20261017*
Code language: PHP (php)
I included -file in the comment field to remind me that these public keys will have a private key that lives somewhere, versus being only stored in hardware and not otherwise accessible. That does not preclude you from also storing them in a Yubikey for more secure usage while keeping the private key files in an offline place. I’ll include a section on that below the Yubikey-based Keys section. You will still need to read through that section in case your hardware key requires setup, and so you know which slots you’ve already used so you don’t overwrite them with these keys.
Yubikey-based SSH Key Concepts
I’m going to talk about some conceptual issues first.
Yubikeys being used for SSH purposes have a concept of slots, PINs, PUKs, management keys, PIN unlocks, and touches.
The important things to know are:
- A PIN can be required for access to load and use keys. When you generate or import a key, you can set it to require a PIN never, every time (always), or once, which actually means either of when it’s first connected or when adding keys back to an ssh agent that had been told to drop them previously.
- A PUK is a different code to unlock a device if the bad PIN entry limit has been reached. If you also lock the PUK out, your keys will no longer be accessible.
- Access to a key slot can be set to require touching the Yubikey never, always, or “cached” which means a fixed 15 second window.
- Unfortunately the PIN requirement is for all slots.
- Different versions of Yubikeys may have different numbers of slots for storing keys.
In my case, my daily use ssh keys were generated in my yubikey. They were never files, so there’s no chance of them being stolen. I have these keys set to requires a touch every time, so if I’m connecting with agent forwarding and a compromised remote system tries to auth as me, it won’t be able to since I won’t touch my yubikey. If I see it flashing, I’ll know something is trying to use it. If my own system were compromised, it would also have to ask me to touch to use the key to ssh auth.
However, I sometimes have needs for bulk ssh’ing, and don’t want to touch the key every time, or every 15 seconds, if I’m running heavily parallel jobs. I have a separate set of rare use ssh keys for those purposes. I keep them in file form in a safe place, and I’ll set them to have a touch policy of NONE, meaning just unlocking the yubikey with PIN and loading the keys will make them available without touch. For my normal use case, when I first connect my Yubikey, I load the keys via PIN, and then I drop these rare use keys from the ssh agent, making them no longer usable. If I want to use them, I have to ssh-add them again, and enter the PIN again.
Yubikey Setup for SSH
Download the Yubikey Manager software from them https://developers.yubico.com/yubikey-manager/Releases/
You’ll also need the piv-tool from https://developers.yubico.com/yubico-piv-tool/Releases/ which is used to extract the public ssh key data after generating them. It’s also necessary for the ssh agent to load them.
I’m going to assume you have no ssh keys on your yubikey yet, but may as well check, and if you’re using more than one you will want to list them, figure out which device serial is which, and include that in the commands as it’s needed if more than one is plugged in.
With their software installed; list them and check settings:
# ykman info
Multiple YubiKeys detected:
- YubiKey 5C (5.7.4) [OTP+FIDO+CCID] Serial: 123456
- YubiKey 5C (5.1.2) [OTP+FIDO+CCID] Serial: 654321
# ykman --device 123456 piv info
PIV version: 5.7.4
PIN tries remaining: 3/3
PUK tries remaining: 3/3
Management key algorithm: AES192
WARNING: Using default PIN!
WARNING: Using default PUK!
WARNING: Using default Management key!
CHUID: No data available
CCC: No data available
# ykman --device 654321 piv info
PIV version: 5.1.2
PIN tries remaining: 3
Management key algorithm: TDES
CHUID: No data available
CCC: No data availableCode language: PHP (php)
The above looks different because the 5.1.2 firmware is fairly old, and is not upgradable. It does not know if the PIN/PUK is default. In any case, you’re checking the above to ensure you don’t have any existing ssh keys you’re about to destroy. If you did have them, you’ll see the slots with a key present:
Slot 9A (AUTHENTICATION):
Private key type: ED25519
Public key type: ED25519
Code language: PHP (php)
If you’re clear to proceed, you’ll start by setting a PIN and PUK, where PIN is the alphanumeric access value, PUK is the one that can reset it if you forget the PIN or enter three bad PINs. If you are not issuing this to a corporate user, you’ll also want to generate an internal management key you will never see (via –protect) and won’t need:
ykman --device 123456 piv reset
ykman --device 123456 piv access change-pin
ykman --device 123456 piv access change-puk
ykman --device 123456 piv access change-management-key --generate --protect
Yubikey SSH Key Generation and Use
Okay, your PIN and PUK are now set, you’re ready to start generating keys. If you’re like me and need all three common types, I’m going to generate those with a policy of PIN once at initial access or future ssh-key additions, and a touch every single time it is used (ALWAYS). There is also the option of pin every time, or never (risk if your key is stolen), and touch never or cached, which means usable for 15 seconds after a touch:
ykman --device 123456 piv keys generate --algorithm ED25519 --pin-policy ONCE --touch-policy ALWAYS 9a 20261017-ed25519-yubi.pem
ykman --device 123456 piv certificates generate --subject "CN=20261017-ed25519-yubi" 9a 20261017-ed25519-yubi.pem
ykman --device 123456 piv keys generate --algorithm ECCP256 --pin-policy ONCE --touch-policy ALWAYS 9c 20261017-ecdsa-yubi.pem
ykman --device 123456 piv certificates generate --subject "CN=20261017-ecdsa-yubi" 9c 20261017-ecdsa-yubi.pem
# Note, this next one will run for a while; like minutes
ykman --device 123456 piv keys generate --algorithm RSA4096 --pin-policy ONCE --touch-policy ALWAYS 9d 20261017-rsa4096-yubi.pem
ykman --device 123456 piv certificates generate --subject "CN=20261017-rsa4096-yubi" 9d 20261017-rsa4096-yubi.pem
Code language: PHP (php)
The certificates step of this will have written out public pem files which you do not need to keep for SSH purposes. You can also generate them at any time, if needed for other purposes, using
ykman piv certificates generate
Next you’ll need to extract the public SSH key components. If you’ve already installed the piv-tool, you’ll have a library you need to pass ssh-keygen. On my Mac it’s as follows, on linux it’s likely /usr/local/lib/libykcs11.so or similar.
ssh-keygen -D /usr/local/lib/libykcs11.dylib | grep ssh
The above will have output your public keys that you can copy/paste into unique files in your normal ~/.ssh/ location with perms 600. You will very likely have an extra RSA key labeled “Public key for PIV Attestation” which is a built-in key and not your SSH key; yours will likely be much longer if you used 4096. Its label may vary depending on which slot you stored it in.
Load your keys into your ssh agent:
# mac-specific library location; will be .so on linux
ssh-add -s /usr/local/lib/libykcs11.dylibCode language: PHP (php)
If you want to get rid of the built-in rsa2048 attestation key so your ssh agent doesn’t offer it, first save to file:
ssh-keygen -D /usr/local/lib/libykcs11.dylib | grep 'Attestation' > ~/.ssh/yubi-attestation.pubCode language: JavaScript (javascript)
Then remove via:
ssh-add -d ~/.ssh/yubi-attestation.pubCode language: JavaScript (javascript)
And alter your day to day process to:
# Load all yubi-managed keys, drop the yubi attestation key
ssh-keygen -D /usr/local/lib/libykcs11.dylib
ssh-add -d ~/.ssh/yubi-attestation.pubCode language: PHP (php)
Storing Existing SSH Keys in Yubikey
For RSA and ECDSA keys, they need a simple conversion that won’t leave them decrypted, but will alter the source files, so do this on a copy of them:
# This converts the actual files, so use a copy of your real key files.
# They do remain password protected.
ssh-keygen -p -m PEM -f 20261017-ecdsa
ssh-keygen -p -m PEM -f 20261017-rsa4096Code language: PHP (php)
Import them into Yubikey; we’ll use slots 82 and 83 since we already used 9a, 9c, 9d earlier in this article:
# ykman --device 123456 piv keys import --pin-policy ONCE --touch-policy NEVER 82 20261017-ecdsa
Enter password to decrypt key:
Enter PIN:
Private key imported into slot RETIRED1.
# ykman --device 123456 piv keys import --pin-policy ONCE --touch-policy NEVER 82 20261017-rsa4096
Enter password to decrypt key:
Enter PIN:
Private key imported into slot RETIRED2.
Code language: PHP (php)
Now remove the temp versions that are no longer in your desired format.
For the ED25519, you can’t import these directly so it’s a bit more involved, and will have a temporarily decrypted version of the key on disk, which is why I suggest using a ramdisk for this. If you’re on a Mac and need ramdisk help:
RAMDEV=$(hdiutil attach -nomount ram://131072 | awk '{print $1}')
diskutil erasevolume HFS+ SSHTMP $RAMDEV
cd /Volumes/SSHTMP
cp ~/.ssh/20261017-ed25519 .
##
## do the below steps to decrypt the key, and then convert its
## format to the import format (2nd decrypted copy)
##
cd
eject $RAMDEVCode language: PHP (php)
So, ramdisk is ready, copy your ed25519 key to it, and:
ssh-keygen -p -N "" -f 20261017-ed25519Code language: JavaScript (javascript)
Your private key no longer has a pass phrase. We’re going to convert it to a format the ykman tool can import:
{ printf '302e020100300506032b657004220420' | xxd -r -p
grep -v -- '-----' 20261017-ed25519 | base64 -d | tail -c +162 | head -c 32
} > ed.der
{ echo '-----BEGIN PRIVATE KEY-----'; base64 < ed.der; echo '-----END PRIVATE KEY-----'; } > ed.pem
Code language: PHP (php)
Two copies of your ed25519 key are currently decrypted on your ramdisk, be careful, do the next steps, clean up. The following will load the key into your yubikey slot 9e, set to require PIN once, and never a touch for usage. Alter as needed.
ykman --device 123456 piv keys import --pin-policy ONCE --touch-policy NEVER 9e ed.pem
rm ed.pem
rm 20261017-ed25519
rm ed.der
# Immediately remove the ram diskCode language: CSS (css)
The public keys will be the same as wherever your on-disk versions live, but if you’ve already tucked them away, or need them on a new system, you can export the public keys from the yubikey via:
ssh-keygen -D /usr/local/lib/libykcs11.dylib
I’d recommend dropping the yubi attestation key into its own public file so you can easily remove it from the ssh agent using the key file. Do the following ONCE:
ssh-keygen -D /usr/local/lib/libykcs11.dylib | grep 'Attestation' > ~/.ssh/yubi-attestation.pubCode language: JavaScript (javascript)
Now you can alter your load routine to:
# Load all yubi-managed keys, drop the yubi attestation key
ssh-keygen -D /usr/local/lib/libykcs11.dylib
ssh-add -d ~/.ssh/yubi-attestation.pubCode language: PHP (php)
Day to Day Yubikey SSH Usage
Unfortunately Yubikeys label their slots with internal non-changeable labels, so you’ll have just exported some keys tagged with crap like:
82 - Public key for Retired Key 1 (ECDSA)
83 - Public key for Retired Key 2 (RSA)
9a - Public key for PIV Authentication (ED25519)
9b - (not usable)
9c - Public key for Digital Signature (ECDSA)
9d - Public key for Key Management (RSA)
9e - Public key for Card Authentication (ED25519)
f9 - Public key for PIV Attestation (RSA)Code language: PHP (php)
If you have some keys you do not normally want available, such as ones you’ve set to have a touch policy of never for rare bulk ssh usage, you’ll need to drop them after your initial ssh-add. Since they won’t have your names, and instead will show these ugly labels, your life will be easier dropping them via public key files that match them.
You’re probably already doing this at first Yubikey connection to your computer:
ssh-keygen -D /usr/local/lib/libykcs11.dylib
ssh-add -d ~/.ssh/yubi-attestation.pubCode language: JavaScript (javascript)
You can simply build on that. Let’s say your day to day usage keys live in slots 9a and 9c, and exist as ~/.ssh/daily1.pub and ~/.ssh/daily2.pub. Your rare use no-touch-required keys live in 82 and 83 and have public keys ~/.ssh/emergency1.pub and ~/.ssh/emergency2.pub. Well your startup routine can just be:
ssh-keygen -D /usr/local/lib/libykcs11.dylib
# enter PIN
ssh-add -d ~/.ssh/yubi-attestation.pub
ssh-add -d ~/.ssh/emergency1.pub
ssh-add -d ~/.ssh/emergency2.pub
ssh-add -lCode language: PHP (php)
That will add all the yubi-managed keys, require PIN, drop the ones you don’t want loaded, and output confirmation.
Legacy Version of the Article
June 2019 update: altered the rounds from 96 to 192. After living with 96 for a couple years, the delay was no longer noticeable, so I moved to a higher number of rounds.
If you’re feeling fancy, you can use the newer ED25519 format, you’d generate it with something along these lines:
ssh-keygen -a 192 -C ed25519-201609 -o -t ed25519 -f identity_file
That would leave you with an identity_file.pub to copy to your authorized_keys file on the remote servers. One thing in the above to take note of is the -a 192 argument. This is used to specify the number of key derivation function rounds to use to protect your private key from brute force attack; it is used when you specify the -o argument which stores the private key in ed25519 format; aka ‘new openssh format’. The benefit here is that the ‘old’ way was using MD5 to protect your key, but with MD5 being easily processed by graphics card GPU’s, it’s, in theory, trivial to brute force or rainbow attack a stolen ssh private key file. By applying a KDF to your private key and using a suitable number of rounds, you can make it much more difficult for someone to attack your file. The default is I believe 16, which already goes from billions per second to perhaps a few attempts per second. I decided to increase the number until I found the delay in authenticating my valid password to be just slightly less than annoying; 128 is where I ended up. Going further and the delay is very noticeable.
Unfortunately, I’ve found some remote systems I want to SSH to do not work with ED25519 keys yet; certain network equipment for example, so after all was said and done, I ended up back on RSA keys. That doesn’t mean you have to throw away the benefit of securing your key with ED25519 though; just use the -o and the -a args to get the same benefits there. Here’s how to generate a 4096-bit RSA key stored with ED215519 and 192 rounds:
ssh-keygen -a 192 -b 4096 -C rsa4096-201609 -o -t rsa -f identity_file