Overview
I wanted a way to secure my SSH private key on a hardware token, so I configured a Yubikey to store the key. This is an overview of the the steps I took to do this.
Install required software
GPG
If you don’t already have GPG installed, you can get it from https://gnupg.org or install it via your favorite package manager. To install it via Homebrew:
$ brew install gpg2
Pinentry (for Mac)
We’ll also want to install a suitable pinentry program to use when authenticating to a remote server. There are GUI and CLI versions; I like the curses version, and that’s what the config settings below will use.
$ brew install pinentry-mac
Yubikey Manager
The Yubikey manager CLI utility will allow us to confirm that the OpenPGP application is enabled on our Yubikey. If you have the graphical manager already installed, this isn’t strictly necessary.
$ brew install ykman
Verify you have a supported Yubikey
Before proceeding, make sure your Yubikey supports OpenPGP and that OpenGPG is enabled:
$ ykman info
Device type: YubiKey 5 NFC
Serial number: XXXXXXXX
Firmware version: 5.2.7
Form factor: Keychain (USB-A)
Enabled USB interfaces: OTP, FIDO, CCID
NFC transport is enabled.
Applications USB NFC
FIDO2 Enabled Enabled
OTP Enabled Enabled
FIDO U2F Enabled Enabled
OATH Enabled Enabled
YubiHSM Auth Not available Not available
OpenPGP Enabled Enabled
PIV Enabled Enabled
The only thing we really care about in this output is the “OpenPGP” line – it should be enabled for USB mode. If the output says “Not available”, your Yubikey doesn’t support the OpenPGP application. If it is disabled, we’ll need to enable it:
$ ykman config usb -e OPENPGP
Enable OpenPGP.
Configure USB? [y/N]: y
Configure Yubikey/GPG to store a GPG key
If you don’t already have a GPG key, you’ll need to generate one with gpg --generate-key
:
$ gpg --generate-key
gpg (GnuPG/MacGPG2) 2.2.3; Copyright (C) 2017 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Note: Use "gpg --full-generate-key" for a full featured key generation dialog.
GnuPG needs to construct a user ID to identify your key.
Real name: Bob Smith
Email address: [email protected]
You selected this USER-ID:
"Bob Smith <[email protected]>"
Change (N)ame, (E)mail, or (O)kay/(Q)uit? O
<output elided>
pub rsa2048 2021-12-29 [SC] [expires: 2023-12-29]
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
uid Bob Smith <[email protected]>
sub rsa2048 2021-12-29 [E] [expires: 2023-12-29]
If you already have a key, get the key id with gpg --list-secret-keys
:
$ gpg --list-secret-keys
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: next trustdb check due at 2023-01-18
/Users/marcusb/.gnupg/pubring.gpg
---------------------------------
sec rsa4096 2016-08-12 [SC] [expires: 2023-01-18]
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
uid [ultimate] Marcus Butler <[email protected]>
ssb rsa4096 2016-08-12 [E] [expires: 2023-01-18]
Now, we need to move the keys to our Yubikey and generate an authentication key to use with
SSH. We’ll run gpg --expert --edit-key <key id>
to do this.
There are a few choices to make when you create your authentication key, namely the key type, key purpose (signing, authentication, encrypt,) cipher-suite-specific details like the key length or elliptic curve to use, and the key lifetime. In my case, I generated a key with the following characteristics:
- ECC Key (option 11)
- Authentication use only (toggle signing off, toggle authentication on)
- Curve 25519 (option 1)
- 2 year lifetime.
While you can pick different options for key type, cipher suite, etc. the key must be configured for the authentication capability in order to be used as an SSH authentication key.
Unless your SSH software does not support it, I think Curve 25519 is the best option to use, based on the compact key size and widespread analysis of the security provided by Curve 25519.
$ gpg --expert --edit-key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
gpg (GnuPG/MacGPG2) 2.2.3; Copyright (C) 2017 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Secret key is available.
sec rsa4096/XXXXXXXXXXXXXXXX
created: 2016-08-12 expires: 2023-01-18 usage: SC
trust: ultimate validity: ultimate
ssb rsa4096/XXXXXXXXXXXXXXXX
created: 2016-08-12 expires: 2023-01-18 usage: E
[ultimate] (1). Marcus Butler <[email protected]>
gpg> addkey
Please select what kind of key you want:
(3) DSA (sign only)
(4) RSA (sign only)
(5) Elgamal (encrypt only)
(6) RSA (encrypt only)
(7) DSA (set your own capabilities)
(8) RSA (set your own capabilities)
(10) ECC (sign only)
(11) ECC (set your own capabilities)
(12) ECC (encrypt only)
(13) Existing key
Your selection? 11
Possible actions for a ECDSA/EdDSA key: Sign Authenticate
Current allowed actions: Sign
(S) Toggle the sign capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? S
Possible actions for a ECDSA/EdDSA key: Sign Authenticate
Current allowed actions:
(S) Toggle the sign capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? A
Possible actions for a ECDSA/EdDSA key: Sign Authenticate
Current allowed actions: Authenticate
(S) Toggle the sign capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? Q
Please select which elliptic curve you want:
(1) Curve 25519
(3) NIST P-256
(4) NIST P-384
(5) NIST P-521
(6) Brainpool P-256
(7) Brainpool P-384
(8) Brainpool P-512
(9) secp256k1
Your selection? 1
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 2y
Key expires at Fri Dec 29 10:48:06 2023 CST
Is this correct? (y/N) y
Really create? (y/N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
sec rsa4096/XXXXXXXXXXXXXXXX
created: 2016-08-12 expires: 2023-01-18 usage: SC
trust: ultimate validity: ultimate
ssb rsa4096/XXXXXXXXXXXXXXXX
created: 2016-08-12 expires: 2023-01-18 usage: E
ssb ed25519/XXXXXXXXXXXXXXXX
created: 2021-12-29 expires: 2023-12-29 usage: A
[ultimate] (1). Marcus Butler <[email protected]>
gpg> keytocard
Really move the primary key? (y/N) y
Please select where to store the key:
(1) Signature key
(3) Authentication key
Your selection? 1
sec rsa4096/XXXXXXXXXXXX3576
created: 2016-08-12 expires: 2023-01-18 usage: SC
trust: ultimate validity: ultimate
ssb rsa4096/XXXXXXXXXXXX6851
created: 2016-08-12 expires: 2023-01-18 usage: E
ssb ed25519/XXXXXXXXXXXX676F
created: 2021-12-29 expires: 2023-12-29 usage: A
[ultimate] (1). Marcus Butler <[email protected]>
gpg> key XXXXXXXXXXXX6851
sec rsa4096/XXXXXXXXXXXX3576
created: 2016-08-12 expires: 2023-01-18 usage: SC
trust: ultimate validity: ultimate
ssb* rsa4096/XXXXXXXXXXXX6851
created: 2016-08-12 expires: 2023-01-18 usage: E
ssb ed25519/XXXXXXXXXXXX676F
created: 2021-12-29 expires: 2023-12-29 usage: A
[ultimate] (1). Marcus Butler <[email protected]>
gpg> keytocard
Please select where to store the key:
(2) Encryption key
Your selection? 2
sec rsa4096/XXXXXXXXXXXX3576
created: 2016-08-12 expires: 2023-01-18 usage: SC
trust: ultimate validity: ultimate
ssb* rsa4096/XXXXXXXXXXXX6851
created: 2016-08-12 expires: 2023-01-18 usage: E
ssb ed25519/XXXXXXXXXXXX676F
created: 2021-12-29 expires: 2023-12-29 usage: A
[ultimate] (1). Marcus Butler <[email protected]>
gpg> key XXXXXXXXXXXX676F
sec rsa4096/XXXXXXXXXXXX3576
created: 2016-08-12 expires: 2023-01-18 usage: SC
trust: ultimate validity: ultimate
ssb rsa4096/XXXXXXXXXXXX6851
created: 2016-08-12 expires: 2023-01-18 usage: E
ssb* ed25519/XXXXXXXXXXXX676F
created: 2021-12-29 expires: 2023-12-29 usage: A
[ultimate] (1). Marcus Butler <[email protected]>
gpg> keytocard
Please select where to store the key:
(3) Authentication key
Your selection? 3
sec rsa4096/XXXXXXXXXXXX3576
created: 2016-08-12 expires: 2023-01-18 usage: SC
trust: ultimate validity: ultimate
ssb rsa4096/XXXXXXXXXXXX6851
created: 2016-08-12 expires: 2023-01-18 usage: E
ssb* ed25519/XXXXXXXXXXXX676F
created: 2021-12-29 expires: 2023-12-29 usage: A
[ultimate] (1). Marcus Butler <[email protected]>
gpg> save
Configure GPG Agent to support SSH and SSH to use GPG Agent
Add the following lines to your ~/.gnupg/gpg-agent.conf file:
default-cache-ttl 600
max-cache-ttl 7200
enable-ssh-support
default-cache-ttl-ssh 600
max-cache-ttl-ssh 600
pinentry-program /usr/local/bin/pinentry-curses
Now, set your shell startup file to start the gpg-agent, if it isn’t running, and to set environment variables that tell the SSH key agent to query gpg-agent for authentication requests:
gpg-agent --daemon >/dev/null 2>&1
export SSH_AUTH_SOCK=$HOME/.gnupg/S.gpg-agent.ssh
export GPG_TTY=$(tty)
GPG Agent is probably already running and needs to be restarted to properly sign authentication requests. You should only need to do this one time:
$ gpgconf --kill gpg-agent
$ gpg-agent --daemon >/dev/null 2>&1
Extract your SSH public key
The last thing to do is extract your public key and add it the authorized_keys file on your SSH servers:
$ ssh-add -L
ssh-ed25519 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX cardno:000000000000
After you do that, SSH should leverage your new key. You can test this by connecting to an SSH server and verifying it is asking for your Yubikey PIN, then removing your Yubikey from your computer and verifying SSH is either unable to authenticate, or is selecting another key or authentication method.
Using your key on a new/different computer
If you’ll be using your key with multiple machines, you’ll need to import the key stub after installing GPG, pinentry, etc. on your new machine:
$ gpg --card-edit
... output elided ...
gpg/card> fetch
... output elided ...
gpg/card> quit
Troubleshooting
Sometimes ssh sessions will hang on startup and eventually display an error like this:
$ ssh jupiter
sign_and_send_pubkey: signing failed for ED25519 "cardno:XXXXXXXXXXXX" from agent: agent refused operation
The only solution I’ve found is to restart the GPG agent:
$ killall -9 gpg-agent;gpg-agent --daemon