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:
- LUKS full disk encryption
- Local Linux login and sudo
- SSH keys for remote login
- Remote sudo over SSH
- GPG private key storage
- AWS CLI authentication
- Encrypted email with Thunderbird
- Using Yubikey-backed keys with Git/Github
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
- Install libraries and tools
- Verify you ave a supported Yubikey
- Setup your U2F keys
- LUKS Full disk encryption setup
- Local login and sudo
- GPG Private Key Storage
- SSH Key Setup
- Remote Sudo with SSH Agent Forwarding
- Encrypted Email with GPG and Thunderbird
- Git/Github Authentication/Code Signing
- 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:
- Ignore the warning about the device not supporting user verification.
- If you fat-finger the argument to fido2-with-user-verification, systemd-cryptenroll will just silently not enforce verification. It will appear to accept arguments like “yes” or “enabled”, but when you unlock the disk, it won’t ask for a password as long as the Yubikey is connected.
- Don’t use the option –fido2-with-client-pin; almost no Yubikeys support that option.
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:
- Make sure you didn’t leave udev in your mkinitcpio.conf HOOK list like I did when setting up sdencrypt. You’ll get a cryptic error on boot about no usb ports available. To continue, you’ll have to edit your grub boot options and remove the rk.luks.options field.
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:
- Using the user’s Unix password as the primary factor and the U2F key as a second factor.
- Using the U2F pin as the primary factor and U2F key presence as the second factor.
- 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.
- 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
- 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
- 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:
- 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
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
- Generate the key:
[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.
- 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
- Set execute permissions on this script
$ sudo chmod +x /usr/local/bin/is_ssh
- 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
- 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
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.
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.
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:
- 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 - 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.