Using a Yubikey to Secure SSH on macOS (Minimalist Version)

SSH is critical in most people’s devops process, be it remote server logins or Git commits. After reading about one too many stories about companies getting hacked that way, I decided to use Yubikeys to store my private SSH keys.

You can either use the PIV- or OpenPGP module for this purpose. I decided to use the former because it’s better integrated and seems to be more reliable. There are a number of guides available online. They all required some tinkering and small adjustment for macOS. So here is my own complete guide.

Install Dependencies

Start by installing two required packages from Homebrew

$ brew install yubico-piv-tool opensc

Next you need to copy the OpenSC PKCS11 driver to a new location, so SSH-Agent can pick it up. By default Homebrew will symlink it, which does not work.

$ rm /usr/local/lib/opensc-pkcs11.so
$ cp $(brew list opensc | grep lib/opensc-pkcs11.so) /usr/local/lib/opensc-pkcs11.so

As a last setup step, add the following line to ~/.ssh/config, so SSH will pick it up when authenticating to a remote server. You can either add it at the top or below a Host example.com block to only apply to that host.

PKCS11Provider /usr/local/lib/opensc-pkcs11.so

Generate Private Keys and Store on Yubikey

You could generate the private key directly on the Yubikey and it will never leave the key. This is great for security but also means you can’t make a backup or copy it to a second Yubikey as backup. For that reason we will securely generate a private SSH key on a RAM disk and then copy it to two Yubikeys.

Start by creating a RAM disk and going into the mount point

$ diskutil erasevolume HFS+ RAMDisk `hdiutil attach -nomount ram://2048`
$ cd /Volumes/RAMDisk

Next generate a new private RSA key (only this specific format and length is supported) and a public key and certificate in the correct format.

$ ssh-keygen -m PEM -t rsa -b 2048 -o -a 100 -C yubikey -f yubikey
$ ssh-keygen -e -f ./yubikey.pub -m PKCS8 > yubikey.pub.pkcs8
$ yubico-piv-tool -a verify-pin -a selfsign-certificate -s 9a -S "/CN=SSH key/" --valid-days=3650 -i yubikey.pub.pkcs8 -o cert.pem

You should now see four files on your RAM disk. The commands below will copy the private key to a Yubikey and also add the self-signed certificate. The last step is mostly to comply with the PIV standard and not really related to the SSH login we want. You can repeat this step for every additional Yubikey you want to seed with this particular private SSH key.

You can customize the touch- and PIN policy to your linking. The command below requires a touch whenever the key is used.

$ yubico-piv-tool -s 9a --pin-policy=once --touch-policy=always -a import-key -i yubikey
$ yubico-piv-tool -a verify -a import-certificate -s 9a -i cert.pem

Using the Yubikey for SSH Logins

Now you are ready to log in to a remote server using the private SSH key stored on the Yubikey. To test the new setup, add the public key to ~/.ssh/authorized_keys or any other place appropriate for the service you are using. You can view the public key using either of those commands, even after you remove the RAM disk.

$ cat ./yubikey.pub  # public key saved on RAM disk
$ ssh-keygen -D /usr/local/lib/opensc-pkcs11.so  # dump directly from Yubikey

After adding the public key to a test server, log in like this:

$ ssh -v -I /usr/local/lib/opensc-pkcs11.so

If it works, you will see those lines and the Yubikey will start flashing to signal it’s waiting for a touch.

debug1: Offering public key: /usr/local/lib/opensc-pkcs11.so RSA SHA256:aeq9rAsbxxxxxxxFWG4 token agent
debug1: Server accepts key: /usr/local/lib/opensc-pkcs11.so RSA SHA256:aeq9rAsbxxxxxxFWG4 token agent

For convenience, you can link your hardware key with SSH-agent to avoid entering the PIN all the time. The first command will load the key, the second one will unload it. This will even survive prolonged hibernation. If someone removes the key or restarts the machine, a PIN will be required.

$ ssh-add -s /usr/local/lib/opensc-pkcs11.so  # add key
$ ssh-add -e /usr/local/lib/opensc-pkcs11.so  # remove key
$ ssh-add -L  # list available keys with public key

Now you should be ready to use the new, secure SSH key in production. Be sure to keep a backup on a second Yubikey in a save place and unmount the RAM disk after validating it works.

Here some usage ideas. You can use the key in any place that uses SSH.

  • SSH login to important production servers
  • Secure SSH proxy to a bastion inside a private network
  • Secure backups with BorgBase.com. You could set all server-keys as append-only and use the Yubikey for full access for pruning.
  • Login to a Git code repo. Be sure to use SSH, not HTTPS.

Resources

Local and remote backups for macOS and Linux using BorgBackup

Updates:

  • Oct 2018: there is now a more detailed guide available for macOS.
  • Sept 2018: there is now a hosting solution for Borg repos. See this post

When I recently switched my Macbook, I got very frustrated with Time Machine. I had used it for occasional local backups of my home folder and was planning to move my data from the new to the old machine.

Unfortunately, the Migration Assistant failed to even find my Time Machine drive and I ended up simply rsyncing everything from the Time Machine backup to a new user folder. After that was done I added a new user in macOS and it just ran a chmod over the whole folder.

After this experience it’s clear that you could as well do your backups with any tool that backs up files, while saving yourself the trouble of Time Machine completely.

The best tool I found for the job is BorgBackup. It supports compression, deduplication, remote backups and many kinds of filters. Doing an incremental backup of 1m files takes about 5 minutes locally. Here are some rough steps to get you started:

  1. Install Borg via Homebrew brew cask install borgbackup  or Apt apt install borgbackup . If you plan on doing remote backups, Borg needs to be installed on the server as well.
  2. Initialize a new backup. For local backups:
    borg init –encryption=none /Volumes/external-disk/Backups/my-machine  (I’m not using encryption here because the drive is encrypted)For remote backups it’s about the same:
    borg init –encryption=none my-backup-host.net:/backups/my-machine 
  3. Next create a file called ~/.borg-filter . This will have the files you do NOT want to backup. An example:
    *.ab
    */.DS_Store
    */.tox
    /Users/manu/.cocoapods
    /Users/manu/.Trash
    /Users/manu/.pyenv/versions
    /Users/manu/.gem
    /Users/manu/.npm
    /Users/manu/.cpanm

    This will include some folders you can easily recreate.

  4. Last you should prepare a backup command. To backup specific important folders to a remote server, I use something like:
    function borg-backup() {
      NOW=$(date +"%Y-%m-%d_%H-%M")
      borg create -v --stats -C zlib --list --filter=AM --exclude-from ~/.borg-filter $BORG_REPO::$NOW \
        ~/Desktop \
        ~/Documents \
        ~/Pictures \
        ~/Library/Fonts
    
      borg prune -v --list $BORG_REPO --keep-daily=3 --keep-weekly=4 --keep-monthly=12
    }

    This will backup my Desktop, Documents, Pictures and Fonts to a new time-stamped snapshot. The last command will rotate and delete old backups. The variable $BORG_BACKUP  has the repo name chosen in the previous step.

    Also be sure to read the official documentation to tune all options to your needs.