Using a Yubikey and GPG for SSH authentication

Posted: Dec 29, 2021

Tags: yubikey gpg ssh linux macos

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:

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
Name: This will be displayed with your post
Email: This isn't visible to or shared with anyone except me (the site owner)
Comment: