This is how I keep my SSH key(s) secure and offline, stored on encrypted USB flash-drive. After restart, unlock the drive on USB insert, automatically load the key then umount, lock, back to safety.
0. Overview
The whole shebang works as follows:
- Insert USB drive
- UDEV,udisks2,udiskie tools detect and mount USB under /run/media/$USER/keys
- $HOME/.config/systemd/user/keys.service is triggered
- keys.service above executes $HOME/.local/bin/keys.sh script
- keys.sh script adds SSH key to ssh-agent handled by $HOME/.config/systemd/user/ssh-agent.service
- Interactively provide keyfile to decrypt the USB drive and passphrase for SSH key.
- Umount and lock the USB drive
- Party until system restart!
I know there are FIDO/U2F or Smartcard hardware devices that support SSH keys but I like my approach for one main reason: I don't have to carry the USB drive with me all the time, it's only needed when I restart the laptop and this usually happens once a month when I update the entire system and install new Linux Kernel.
1. Visual teaser
2. How it is made
Wipe the USB drive
wipefs --all --backup /dev/sdb # WARNING: erase headers
Partition the drive
gdisk /dev/sdb
Setup encryption
It depends on your hardware but on my machine aes-xts cipher with 256-bit key length is the fastest, also sha256 as password-based key derivation function (PBKDF).
cryptsetup benchmark
# Tests are approximate using memory only (no storage IO). PBKDF2-sha1 1736052 iterations per second for 256-bit key PBKDF2-sha256 2216862 iterations per second for 256-bit key PBKDF2-sha512 1565038 iterations per second for 256-bit key PBKDF2-ripemd160 900838 iterations per second for 256-bit key PBKDF2-whirlpool 674759 iterations per second for 256-bit key argon2i 8 iterations, 1048576 memory, 4 parallel threads (CPUs) for 256-bit key (requested 2000 ms time) argon2id 8 iterations, 1048576 memory, 4 parallel threads (CPUs) for 256-bit key (requested 2000 ms time) # Algorithm | Key | Encryption | Decryption aes-cbc 128b 1178.6 MiB/s 3409.0 MiB/s serpent-cbc 128b 101.2 MiB/s 753.2 MiB/s twofish-cbc 128b 229.7 MiB/s 405.9 MiB/s aes-cbc 256b 892.9 MiB/s 2818.4 MiB/s serpent-cbc 256b 101.0 MiB/s 753.9 MiB/s twofish-cbc 256b 231.1 MiB/s 411.3 MiB/s aes-xts 256b 3440.2 MiB/s 3416.4 MiB/s serpent-xts 256b 665.6 MiB/s 670.0 MiB/s twofish-xts 256b 386.4 MiB/s 391.3 MiB/s aes-xts 512b 2798.4 MiB/s 2794.7 MiB/s serpent-xts 512b 657.2 MiB/s 665.8 MiB/s twofish-xts 512b 385.7 MiB/s 386.3 MiB/s
Create LUKS partition with given cipher, length and hash function.
cryptsetup --cipher aes-xts-plain64 --key-size 256 --hash sha256 luksFormat /dev/sdb1
When you create LUKS partition you will be asked to provide a passphrase but optionally you can add multiple passphrases or keyfiles.
cryptsetup luksAddKey /dev/sdb1 # add new passphrase
cryptsetup luksAddKey /dev/sdb1 /path/to/file # add new keyfile
Open our encrypted partition (with either passphrase or keyfile) and it will available under /dev/mapper/NAME.
cryptsetup open /dev/sdb1 keys # open with passphrase
cryptsetup --keyfile=/path/to/file open /dev/sdb1 keys # open with keyfile
Create file-system
Standard ext4 linux file-system and set the label for our partition, this is important for auto-mounting later.
mkfs.ext4 /dev/mapper/keys
e2label /dev/mapper/keys keys
lsblk -f /dev/sdb
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS sdb ├─sdb1 crypto_LUKS 2 40b90bb8-5bf2-44ab-818b-3385ca82b644 │ └─keys ext4 1.0 keys e36e5613-1f17-4161-a9e6-8b0c1dfe9430 └─sdb2
Auto-mount with UDEV, udisks2 and udiskie
Create a file /etc/udev/rules.d/66-keys.rules and add UDEV rules to trigger mount when USB is inserted (I have two USB drives, main one and backup).
# keys1
ACTION=="add", ATTRS{idVendor}=="090c", ATTRS{idProduct}=="1000", NAME=keys
# keys2
ACTION=="add", ATTRS{idProduct}=="1643", ATTRS{idVendor}=="0951", NAME=keys
Reload UDEV daemon to pick up rules changes.
udevadm control --reload
To automatically mount USB drive we need two things:
Start udiskie with the following configuration file.
udiskie -c $HOME/.config/udiskie/config.yml
program_options:
tray: true
menu: nested
notify: true
device_config:
- device_file: /dev/loop*
ignore: true
Systemd keys.service
You can also use RUN action provided by UDEV above to run a script but that is quite limited and this Systemd trigger is a better option.
[Unit]
Description=SSH keys on USB
Requires=run-media-icostan-keys.mount
After=run-media-icostan-keys.mount
[Service]
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
ExecStart=/home/icostan/.local/bin/keys.sh
[Install]
WantedBy=run-media-icostan-keys.mount
Script keys.sh
#!/usr/bin/env sh
set -e
SSH_KEY=id_rsa
SSH_ADD=/usr/bin/ssh-add
USB_LABEL=keys
USB_MOUNT=/run/media/$USER/$USB_LABEL
UMOUNT_BIN=/usr/bin/udiskie-umount
$SSH_ADD $USB_MOUNT/$SSH_KEY
notify-send "Added $SSH_KEY to ssh-agent!"
$UMOUNT_BIN $USB_MOUNT
Systemd ssh-agent.service
[Unit]
Description=SSH Agent
[Service]
Type=simple
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
# DISPLAY required for ssh-askpass to work
Environment=DISPLAY=:0
ExecStart=/usr/bin/ssh-agent -D -a $SSH_AUTH_SOCK
[Install]
WantedBy=default.target
SSH key in ssh agent
Finally, here is the key fingerprint (redacted) in SSH agent.
ssh-add -l
3072 SHA256:--5-T7Z4/k-dqfyoXw-8Li-fB-5r-4F0-wCyfQ-ccJ- icostan@drakarys (RSA)
Security
Attackers still can:
- dump ssh-agent's memory and extract the key - but they need root on my box
- key logger - at the exact same time when I select keyfile and fill in passphrase
Secure? so, so … but still a lot better than keeping SSH keys in $HOME/.ssh all the time.
References
- https://wiki.archlinux.org/title/Device_file#Utilities
- https://wiki.archlinux.org/title/Dm-crypt/Drive_preparation
- https://wiki.archlinux.org/title/Dm-crypt/Device_encryption
- https://wiki.archlinux.org/title/Udisks
- https://github.com/coldfix/udiskie/wiki/Usage
- https://wiki.archlinux.org/title/Udev
- https://wiki.archlinux.org/title/SSH_keys
- https://www.funtoo.org/OpenSSH_Key_Management,_Part_3
- https://blog.ledger.com/ssh/