Consolidated Guide to Using Yubikeys with Linux

Posted: Mar 6, 2024 Updated: Jul 7, 2024

Tags: yubikey linux security u2f gpg ssh

Passwords are terrible, but Yubikeys are awesome, especially for reducing the number of passwords you need to remember and the risk of those passwords being stolen. After a fair amount of experimentation, I’ve landed on the following setup to make the most use of my Yubikeys:

You’ll also naturally get browser-based U2F support, without having to do any extra work. The goals are security, ease (and consistency) of use, breadth of coverage, and relability (mostly by configuring a backup key for each service.)

Note: most of these steps use Yubikey’s U2F mode. Those instructions will probably work with other U2F keys. Other steps, which are clearly identified, involve using the Yubikey smartcard slot with GPG or a TOTP profile. Those steps will probably not work with other U2F keys. YMMV. These steps were written for and tested on Arch Linux; if you use a different distro, some adjustments will need to be made. YMMV.

Table of Contents

  1. Install libraries and tools
  2. Verify you ave a supported Yubikey
  3. Setup your U2F keys
  4. LUKS Full disk encryption setup
  5. Local login and sudo
  6. GPG Private Key Storage
  7. SSH Key Setup
  8. Remote Sudo with SSH Agent Forwarding
  9. Encrypted Email with GPG and Thunderbird
  10. Git/Github Authentication/Code Signing
  11. AWS CLI login

Install libraries and tools

There are a few tools you’ll need to install in order to use everything described in this document. On Arch Linux, you’ll want to install the following:

[marcusb@dom ~]$ pacman -Syu yubikey-manager libfido2 pam-u2f gnupg pinentry opensc pcsclite pcsc-tools
... Package manager output elided ...

If you SSH into an Arch Linux machine and want to use your Yubikey for sudo, you’ll need to install the AUR package for pam_ssh_agent_auth using your favorite AUR package tool.

Verify you have a supported Yubikey

Before going any further, make sure your Yubikey supports U2F and, if you want to store your GPG Private Key, OpenPGP. You’ll need to use ykman for this:

[marcusb@dom ~]$ ykman info
Device type: YubiKey 5Ci
Serial number: 1117XXXX
Firmware version: 5.2.4
Form factor: Keychain (USB-C, Lightning)
Enabled USB interfaces: OTP, FIDO, CCID

Applications
OTP             Enabled
FIDO U2F        Enabled
FIDO2           Enabled
OATH            Enabled
PIV             Enabled
OpenPGP         Enabled
YubiHSM Auth    Not available

Make sure “FIDOU2F” and “OpenPGP” are enabled. If not, enable them:

[marcusb@dom ~]$ ykman config usb -e u2f
USB configuration changes:
  Enable FIDO U2F
Proceed? [y/N]: y

[marcusb@dom ~]$ ykman config usb -e openpgp
Enable OpenPGP.
Configure USB? [y/N]: y

If you don’t already have a Yubikey, or need to get a new one, Yubico has a comparison tool here showing which keys support the various applications. If you only have one Yubikey, I strongly recommend buying a second and enrolling it as a secondary key in each of the steps that follow. That way, if your primary key is lost or damaged, you won’t lose access to your systems.

Setup your U2F keys

By default, Yubikeys don’t have a FIDO PIN set. I recommend setting one so you have an additional authentication factor which should make the loss (especially by theft) of a key less worrisome and make evil maid attacks more difficult to pull off.

Despite the name, this “PIN” is an alpha-numeric password. So, feel free to use a good password with letters, numbers, and special characters, but keep in mind this is something you’ll be typing each time you use the key.

You’ll need to complete this step for each of your keys. To avoid confusion, I recommend only having one key plugged in at a time for this and all of the remaining steps.

[marcusb@dom ~]$ ykman fido access change-pin                                                                                 √
Enter your current PIN:
Enter your new PIN:
Repeat for confirmation:

Please note: in the steps to follow, if you look at the man pages for the tools involved, you’ll see several different options for “PIN Verification”, “User Verification, “Client PINs”, and many similar sounding concepts. There’s also some Yubikey documentation that makes it sound like Yubikeys don’t support a PIN with FIDO U2F at all. As long as you follow the steps I’ve outlined here, you’ll end up with a MFA setup that requires a password in addition to the presence of your Yubikey to work. If you try those other options listed in the man pages, they probably won’t work, and, worse, will fail silently or in unpredictable ways.

LUKS Full disk encryption setup

This section assumes you have already setup LUKS on your root filesystem and are using a static passphrase to unlock it. It also assumes you are using the sd-encrypt initramfs hook to decrypt said root filesystem. If you haven’t setup LUKS yet, and want full disk encryption, check out the guides on the Arch Linux Wiki. Steps on other distros may be slightly different:

I recommend setting up a passphrase to unlock your root filesystem for now; once that is working, and your U2F keys are enrolled and working, you can go back an remove the static passphrase.

Now, you can enroll the first U2F key:

[marcusb@dom ~]$ sudo systemd-cryptenroll /dev/your_filesystem_here --fido2-device=auto --fido2-with-user-verification=true
πŸ” Please enter current passphrase for disk /dev/nvme0n1p2: β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’
Locking with user verification test requested, but FIDO2 device /dev/hidraw11 does not support it, disabling.
Initializing FIDO2 credential on security token.
πŸ‘† (Hint: This might require confirmation of user presence on security token.)
πŸ” Please enter security token PIN: β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’
Generating secret key on FIDO2 security token.
πŸ‘† In order to allow secret key generation, please confirm presence on security token.
New FIDO2 token enrolled as key slot 2.

Repeat this command for your second key. The output should indicate the second key was installed in a different LUKS slot (the last line of the command output.)

Notes:

You also need to tell the sdencrypt hook to use the FIDO device. Edit your /etc/default/grub file and modify the GRUB_CMDLINE_LINUX_DEFAULT line to include this option:

GRUB_CMDLINE_LINUX_DEFAULT="...existing options... rd.luks.options=<your root partition uuid>=fido2-device=auto"

You should already have the UUID on the same line for the rd.luks.name option you set when setting up LUKS, but if you need to look it up for whatever reason, lsblk -f will show you:

[root@dom local]# lsblk -f
NAME        FSTYPE      FSVER LABEL UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
nvme0n1
β”œβ”€nvme0n1p1 vfat        FAT32       7506-FFED                             361.9M    27% /boot
└─nvme0n1p2 crypto_LUKS 2           fe9588c9-6502-4a4e-882a-9c3a7eef6a9f
  └─root    ext4        1.0         51645c3a-f5a1-4f55-891f-4bf650f29313    1.5T    11% /
[root@dom local]#

Note that the UUID to use for both the rd.luks.name and rd.luks.options fields is the one associated with the physical partition (the nvme0n1p2 line in the above output) – not the one associated with the encrypted filesystem (the “root” line in the above output.)

Regenerate the Grub config file:

[marcusb@dom ~]$ sudo bash
Password:
[root@dom local]# grub-mkconfig > /boot/grub/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-linux
Found initrd image: /boot/initramfs-linux.img
Found fallback initrd image(s) in /boot:  initramfs-linux-fallback.img
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
Adding boot menu entry for UEFI Firmware Settings ...
done

Rebuild your initramfs, just for good measure:

[root@dom local]# mkinitcpio -P
==> Building image from preset: /etc/mkinitcpio.d/linux.preset: 'default'
==> Using default configuration file: '/etc/mkinitcpio.conf'
  -> -k /boot/vmlinuz-linux -g /boot/initramfs-linux.img --microcode /boot/*-ucode.img
==> Starting build: '6.6.10-arch1-1'
  -> Running build hook: [base]
  -> Running build hook: [systemd]
  -> Running build hook: [autodetect]
  -> Running build hook: [modconf]
  -> Running build hook: [kms]
  -> Running build hook: [keyboard]
==> WARNING: Possibly missing firmware for module: 'xhci_pci'
  -> Running build hook: [sd-vconsole]
  -> Running build hook: [keymap]
  -> Running build hook: [consolefont]
==> WARNING: consolefont: no font found in configuration
  -> Running build hook: [block]
  -> Running build hook: [sd-encrypt]
  -> Running build hook: [filesystems]
  -> Running build hook: [fsck]
==> Generating module dependencies
==> Creating zstd-compressed initcpio image: '/boot/initramfs-linux.img'
==> Image generation successful
==> Building image from preset: /etc/mkinitcpio.d/linux.preset: 'fallback'
==> Using default configuration file: '/etc/mkinitcpio.conf'
  -> -k /boot/vmlinuz-linux -g /boot/initramfs-linux-fallback.img -S autodetect --microcode /boot/*-ucode.img
==> Starting build: '6.6.10-arch1-1'
  -> Running build hook: [base]
  -> Running build hook: [systemd]
  -> Running build hook: [modconf]
  -> Running build hook: [kms]
==> WARNING: Possibly missing firmware for module: 'ast'
  -> Running build hook: [keyboard]
==> WARNING: Possibly missing firmware for module: 'xhci_pci'
  -> Running build hook: [sd-vconsole]
  -> Running build hook: [keymap]
  -> Running build hook: [consolefont]
==> WARNING: consolefont: no font found in configuration
  -> Running build hook: [block]
==> WARNING: Possibly missing firmware for module: 'qed'
==> WARNING: Possibly missing firmware for module: 'qla1280'
==> WARNING: Possibly missing firmware for module: 'qla2xxx'
==> WARNING: Possibly missing firmware for module: 'bfa'
==> WARNING: Possibly missing firmware for module: 'wd719x'
==> WARNING: Possibly missing firmware for module: 'aic94xx'
  -> Running build hook: [sd-encrypt]
  -> Running build hook: [filesystems]
  -> Running build hook: [fsck]
==> Generating module dependencies
==> Creating zstd-compressed initcpio image: '/boot/initramfs-linux-fallback.img'
==> Image generation successful
[root@dom local]#

Notes:

Local login/local sudo

Register your tokens

pamu2fcfg -u<yourusername>

[marcusb@dom ~]$ pamu2fcfg -umarcusb
Enter PIN for /dev/hidraw6:
marcusb:<base64 key output elided>,es256,+presence% **<--- don't paste the trailing % if you see it**

Run that for each of your tokens, and place the output in /etc/u2f_users. There should be one line per user; if you have multiple keys, the output of pamu2fcfg for secondary/tertiary keys should be appended to the output for the first key with the username removed, but a leading colon present as a separator:

marcusb:<key 1 base 64 key data>,es256,+presence:<key 2 base 64 key data>,es256,+presence:<key 3 base64 key data>,es256,+presence...

Fix permissions on /etc/u2f_users:

chmod 644 /etc/u2f_users

Non-privileged authentication programs, like swaylock, will fail if they cannot read this file.

Configure PAM to use the u2f module

There are a few common ways to configure this depending on the user experience you want:

  1. Using the user’s Unix password as the primary factor and the U2F key as a second factor.
  2. Using the U2F pin as the primary factor and U2F key presence as the second factor.
  3. Using the U2F key presence without requiring a PIN/password at all.

You could also set these on a service-by-service basis, but these instructions assume you want the same auth policy across all services, so we edit the /etc/pam.d/system-auth file.

  1. Edit /etc/pam.d/system-auth Edit this line:
auth       [success=1 default=bad]     pam_unix.so          try_first_pass nullok

to be:

auth       [success=ok default=die]     pam_unix.so          try_first_pass nullok

Then add this line immediately afterwards:

auth        sufficient                  pam_u2f.so           authfile=/etc/u2f_users cue userpresence=1
  1. Edit /etc/pam.d/system-auth and comment out or remove this line:
auth       [success=1 default=bad]     pam_unix.so          try_first_pass nullok

In its place, add this:

auth        sufficient                  pam_u2f.so           authfile=/etc/u2f_users cue pinverification=1
  1. Edit /etc/pam.d/system-auth and comment out or remove this line:
auth       [success=1 default=bad]     pam_unix.so          try_first_pass nullok

In its place, add this:

auth        sufficient                  pam_u2f.so           authfile=/etc/u2f_users cue userpresence=1

GPG Private Key Storage

This section covers storing your private key on your Yubikey(s), and preparing to use a Yubikey-backed GPG private key for SSH authentication.

Create a new GPG key (if you don’t already have one)

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]

Extract the key id

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]

Generate an SSH key and move the private keys to your Yubikey(s)

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

In order to support GPG-backed SSH keys, or to use a Yubikey-backed GPG private key with Thunderbird, we need to configure the 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-gtk-2

Add the following lines to your ~/.gnupg/scdaemon.conf file:

pcsc-driver /usr/lib/libpcsclite.so
card-timeout 5
disable-ccid
pcsc-shared

Make OpenSC always use the OpenPGP driver for your Yubikey. If you don’t do this, you’ll likely run into problems switching between U2F and GPG uses of your Yubikey (one or the other will start to fail.)

Run pcsc_scan to get the attribute ID of your Yubikey(s):

$ pcsc_scan
PC/SC device scanner
V 1.7.1 (c) 2001-2022, Ludovic Rousseau <[email protected]>
Using reader plug'n play mechanism
Scanning present readers...
0: Yubico YubiKey OTP+FIDO+CCID 00 00

Wed Mar  6 10:40:50 2024
 Reader 0: Yubico YubiKey OTP+FIDO+CCID 00 00
  Event number: 0
  Card state: Card inserted,
  ATR: 3B FD 13 00 00 81 31 FE 15 80 73 C0 21 C0 57 59 75 62 69 4B 65 79 40

... output elided

Place an entry in /etc/opensc.conf forcing it to use the OpenPGP driver for your Yubikey:

card_atr 3B:FD:13:00:00:81:31:FE:15:80:73:C0:21:C0:57:59:75:62:69:4B:65:79:40 {
         name = "Yubikey USB-C";
         driver = "openpgp";
}

(Note the requirement to change from space- to colon-delimited format.)

Make sure the GPG agent service is running:

systemctl --user enable --now gpg-agent

Now, set your shell startup file to set environment variables that tell the SSH key agent to query gpg-agent for authentication requests. I also like to include an alias to update the startup tty on demand:

    if [ ! $SSH_TTY ]; then
        export SSH_AUTH_SOCK=/var/run/user/$(id -u)/gnupg/S.gpg-agent.ssh
        export GPG_TTY=($tty)
        gpg-connect-agent updatestartuptty /bye
    fi
    
    alias gpgtty='gpg-connect-agent updatestartuptty /bye'

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

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

SSH Keys

Overall, two options are presented here: using U2F-backed ed25519-sk keys and using GPG-backed ed25519 keys with the private keys being stored on the Yubikey. Personally, I use the GPG method, as that was all that was supported when I first set this up. It also works well with remote sudo.

GPG-backed keys

We did almost everything in the GPG section above. To actually use your key, extract it from the GPG agent 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 if Pinentry 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.

U2F Keys with SSH

[marcusb@dom ~]$ ssh-keygen -t ed25519-sk -f .ssh/yubikey1_id_ed25519_sk                                                     ?1
Generating public/private ed25519-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter PIN for authenticator:
You may need to touch your authenticator again to authorize key generation.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in .ssh/yubikey1_id_ed25519_sk
Your public key has been saved in .ssh/yubikey1_id_ed25519_sk.pub
The key fingerprint is:
SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX marcusb@dom
The key's randomart image is:
+[ED25519-SK 256]-+
|                 |
|         *       |
|        . .      |
|         =     o |
|     .  So+ + o  |
|     .=o+ .+ + + |
|    o.o*..S . =  |
| ..+o++ .* = =.o |
|o++=S.. . E.+.o. |
+----[SHA256]-----+

From there, you’ll have a private key stored in .ssh/yubikey1_id_ed25519_sk and a public key in .ssh/yubikey1_id_ed25519_sk.pub; copy the public key into your various authorized_keys files to start using the new key. Note that while the private key is (partially???) stored on your computer, the Yubikey must be present to (sign the transaction???)

Remote Sudo with SSH Agent Forwarding

At this point, you should be able to login to your remote machines with your Yubikey-backed SSH keys. If you want to use them to authenticate remote sudo sessions, you’ll need to install and setup the pam_ssh_agent_auth module and setup SSH agent forwarding.

Install the SSH Agent Auth PAM Module

On Amazon Linux, run

dnf install pam_ssh_agent_auth

On Arch, there is a package in AUR: https://aur.archlinux.org/pam_ssh_agent_auth.git

If your distro doesn’t have a suitable package, the source can be found here.

Configure PAM to use the SSH Agent Auth Module on a machine without local (non-SSH) sessions to consider

Edit /etc/pam.d/sudo. Comment out the existing auth lines.

Add:

auth    sufficient      pam_ssh_agent_auth.so file=~/.ssh/authorized_keys

Edit /etc/sudoers

Find your Defaults env_reset/env_keep section and add the following line:

Defaults    env_keep += "SSH_AUTH_SOCK"

Note: it is easy to lock yourself out while doing this. It is a very good idea to ssh in and start a sudo bash session in another terminal while you complete these steps so you can recover from any errors.

Configure PAM to use the SSH Agent Auth Module on a machine WITH local (non-SSH) sessions to consider

There are two options: first, use the same solution as above. This will use the SSH authentication agent for both local and remote sudo sessions. If you are using GPG-backed keys, this will probably result in a different authentication experience for local login vs sudo access. If you want to use the SSH authentication agent for remote sudo sessions but keep your normal experience for local sudo sessions, then you need to configure a few extra items.

  1. Save this script as /usr/local/bin/is_ssh to identify SSH vs non-SSH sessions.
#!/bin/bash

PROCS=$(ps -ef)

echo $PROCS | grep "sshd: ${PAM_USER}@${PAM_TTY:5}" >/dev/null 2>&1
if [ $? -eq 0 ]; then
  exit 1
else
  exit 0
fi
  1. Set execute permissions on this script
$ sudo chmod +x /usr/local/bin/is_ssh
  1. Edit your /etc/pam.d/sudo file to use the pam_u2f module for login sudo sessions and pam_ssh_agent_auth for remote sudo session:
auth       required                    pam_faillock.so      preauth
# Optionally use requisite above if you do not want to prompt for the password
# on locked accounts.
-auth      [success=3 default=ignore]  pam_systemd_home.so
auth       [success=1 default=ignore]  pam_exec.so quiet /usr/local/bin/is_ssh
auth       [success=2 default=die]                     pam_ssh_agent_auth.so file=~/.ssh/authorized_keys
auth       sufficient                  pam_u2f.so           authfile=/etc/u2f_users cue pinverification=1
auth       [default=die]               pam_faillock.so      authfail
auth       optional                    pam_permit.so
auth       required                    pam_env.so
auth       required                    pam_faillock.so      authsucc

Yours may be a bit different; the three most important lines are:

auth       [success=1 default=ignore]  pam_exec.so quiet /usr/local/bin/is_ssh
auth       [success=2 default=die]                     pam_ssh_agent_auth.so file=~/.ssh/authorized_keys
auth       sufficient                  pam_u2f.so           authfile=/etc/u2f_users cue pinverification=1

Enabling agent forwarding

Agent forwarding should generally only be enabled when needed, in this case, when you think you’ll need to invoke sudo on a remote host. To enable agent forwarding for a particular connection, pass the -A flag to ssh:

[marcusb@dom ~]$ ssh -A luna.marcusb.org                                                                                         √
Last login: Mon Jan  8 16:46:37 2024 from somewhere
[marcusb@luna ~]$ set|grep AUTH
SSH_AUTH_SOCK=/tmp/ssh-XXXX472KsF/agent.1894338
[marcusb@luna ~]$

You can also enable it on a host-by-host basis in your ~/.ssh/config file:

Host luna.marcusb.org
    ForwardAgent yes
[marcusb@dom ~]$ ssh luna.marcusb.org                                                                                      ?255
Last login: Mon Jan  8 16:46:59 2024 from somewhere
[marcusb@luna ~]$ set|grep AUTH
SSH_AUTH_SOCK=/tmp/ssh-XXXX8cjhto/agent.1894505
[marcusb@luna ~]$ exit
logout
Connection to luna.marcusb.org closed.
[marcusb@dom ~]$ ssh admin@sol
[admin@sol ~]$ set|grep AUTH
[admin@sol ~]$ exit

Or, just enable it outright for every outbound SSH connection in your ~/.ssh/config file, although this is not recommended:

ForwardAgent yes

Encrypted email with Thunderbird

There are three steps required to setup Thunderbird to use your Yubikey-backed GPG key: enable external GPG use, import your public key

  1. Collect the information you need from GPG. In your terminal, run gpg --list-signatures and note the 16 digit signature of your public key:
$ gpg --list-signatures [email protected]                                                                 √
pub   rsa4096 2016-08-12 [SC] [expires: 2025-06-28]
      9FCFF9AEE365B2A8918FD3576CA3CD34C6363576
uid           [ultimate] Marcus Butler <[email protected]>
sig 3        6CA3CD34C6363576 2016-08-12  [self-signature]
sig 3        6CA3CD34C6363576 2023-06-21  [self-signature]
sub   rsa4096 2016-08-12 [E] [expires: 2025-06-28]
sig          6CA3CD34C6363576 2023-06-21  [self-signature]

In my case, the bit on the last line, 6CA3CD34C6363576, is the signature that Thunderbird needs. If you haven’t already, also export your public key:

$ gpg --export --armor [email protected] > public.key
  1. Open Thunderbird Settings, then on the bottom of the General tab, open the config editor. In the search box, enter “mail.openpgp.allow_external_gpg” and change the value to true.

  2. Open Thunderbird Account Settings, then select “End-to-End Encryption” and click “Add Key”. Select “Use your external key through GnuPG” and paste in the 16 digit signature of your key.

  3. Back on the End to End Encryption page, click OpenPGP Key Manager. Click File -> Import Public Key(s) from File. Select the file containing the public key you exported. You’ll probably have to change “GnuPG Files” to “All Files” at the bottom of the file dialog window. Change “Not Accepted” to “Accepted (unverified)”, then click Import. Click View Details and Manage Key Acceptance, and select “Yes, I’ve verified in person this key has the correct fingerprint”.

Now, when composing a message you should be able to select encrypt and/or sign under the OpenPGP menu. When selecting either of those options, you should get a pinentry dialog asking you to enter the PIN for your Yubikey.

Git/Github Authentication/Commit Signing

Git can use your Yubikey-backed GPG key to sign commits and, of course, for SSH authentication to remote repositories (including Github repositories.)

Code signing should work out-of-the-box after completing the GPG steps above whenever you sign your commits with git commit -S. If you want to sign all commits by default, run git config --local commit.gpgsign true.

For repositories on normal SSH servers, you would follow the steps above to add your keys to your authorized_keys file. For Github, the easiest way to add your signing and authentication key is via the website. Login to Github and click on your profile icon, then settings, then SSH and GPG Keys:

  1. For each of your Yubikeys, click on “New SSH key” and add the appropriate line from ssh-add -L, the same as you would in authorized_keys
  2. For your GPG signing key, click “New GPG key”. Run gpg --export --armor to get your public key, and paste in the resulting block of text.

Verify you can commit and push to a repository you control.

AWS CLI Login

This setup uses the AWS STS (Security Token Service) and AssumeRole function to use the Yubikey for time-limited AWS CLI credentials. First, login to the AWS console and open up the IAM page. You need to be very careful, and very sure you understand AWS permissions before completing these steps. I am not responsible for you misconfiguring your permissions or over-exposing your AWS accounts and resources.

Create a Security Token Service (STS) policy

Click Policies, then Create policy Change policy editor to JSON and paste the following policy in:

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "VisualEditor0",
			"Effect": "Allow",
			"Action": "sts:AssumeRole",
			"Resource": "*",
			"Condition": {
				"Bool": {
					"aws:MultiFactorAuthPresent": "true"
				}
			}
		}
	]
}

Click next.

Name the policy and click Create policy.

Create one or more roles to be able to assume

Click Roles, then Create Role

Change the trusted entity type to “Custom trust policy”

Change the custom trust policy to something like this:

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "Statement1",
			"Effect": "Allow",
			"Principal": {
                          "AWS": "arn:aws:iam:YOUR_ACCOUNT_ID:root"
                        },
			"Action": "sts:AssumeRole"
		}
	]
}

Click Next

Select the permissions policies you want to attach to this role from the list, or create a new custom policy.

Click Next

Name the policy and review the policy contents to be sure you are comfortable with what is being exposed.

Copy the ARN of the role – you’ll need it later

Create an AssumeRole user.

Click Users

Click Create user and enter a username.

Click Next

Select Attach policies directly and search for the STS policy you created earlier.

Click Next

Click Create user

Click the user to edit it.

Click “Create access key”

Select “Command Line Interface” as your use case.

Click next

Give the access key a name like assumeroleaccesskey and click Create Access Key

Run “aws configure” and enter the access key and secret access key. Save both in your password manager as appropriate.

$ aws configure
AWS Access Key ID [****************YTEE]: XXXXXXXXXXXXXXXXXXXX
AWS Secret Access Key [****************++EV]: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Default region name [us-east-2]:
Default output format [None]:

Click security credentials

Click assign MFA device

Enter a descriptive device name like “yubikey1”

Select Authenticator app

Click next

Click “show secret key” and copy the key

In your terminal, run

ykman oath accounts add aws <secret key>
ykman oath accounts code -s aws
sleep 30
ykman oath accounts code -s aws

Paste the two codes into the MFA code 1 and MFA code 2 boxes on the form.

Click Add MFA

Note the value in the Identifier column – that’s what you’ll use as the “serial number” to tell AWS which MFA device you are generating codes from.

Verify the user works.

Download my awslogin script here

Create a file in your home directory called ‘.aws-login’ and copy the Assume Role ARN and ARN of your assume role user in the file like so:

AWS_TOTP="aws" # or whatever you named the oath account when running ykman oath accounts add ...
AWS_ROLE_ARN="arn:aws:iam::YOUR_ACCOUNT_ID:role/AssumeRole"
AWS_SESSION_NAME="marcus_session"
AWS_SERIAL_NUMBER="arn:aws:iam::YOUR_ACCOUNT_ID:mfa/yubikey1"

Run the awslogin script. It should output three lines you can paste into your shell with temporary credentials.

Make sure you can successfully – but only – run commands permitted by your chosen role/policy.

Name: This will be displayed with your post
Email: This isn't visible to or shared with anyone except me (the site owner)
Comment: