SSH keys on USB


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:

  1. Insert USB drive
  2. UDEV,udisks2,udiskie tools detect and mount USB under /run/media/$USER/keys
  3. $HOME/.config/systemd/user/keys.service is triggered
  4. keys.service above executes $HOME/.local/bin/ script
  5. script adds SSH key to ssh-agent handled by $HOME/.config/systemd/user/ssh-agent.service
  6. Interactively provide keyfile to decrypt the USB drive and passphrase for SSH key.
  7. Umount and lock the USB drive
  8. 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
├─sdb1   crypto_LUKS 2           40b90bb8-5bf2-44ab-818b-3385ca82b644
│ └─keys ext4        1.0   keys  e36e5613-1f17-4161-a9e6-8b0c1dfe9430

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:

  1. udisks2 - daemon to manipulate storage devices from user-space
  2. udiskie - a front-end for udisks2

Start udiskie with the following configuration file.

  udiskie -c $HOME/.config/udiskie/config.yml
    tray: true
    menu: nested
    notify: true

    - 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.

  Description=SSH keys on USB




  #!/usr/bin/env sh

  set -e


  notify-send "Added $SSH_KEY to ssh-agent!"


Systemd ssh-agent.service

  Description=SSH Agent

  # DISPLAY required for ssh-askpass to work
  ExecStart=/usr/bin/ssh-agent -D -a $SSH_AUTH_SOCK


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)


Attackers still can:

  1. dump ssh-agent's memory and extract the key - but they need root on my box
  2. 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.

SSH  GPG  keys  linux  USB