Using 1Password with ssh-agent on Linux

I run the dev channel of ChromeOS. This crashes occasionally. While my chrome tabs are generally recovered, it also resets the crostini containers. Every time this happens, I’ve to launch 1Password (the android app), unlock it, search for my SSH key, copy the password and finally paste it in the terminal. This was starting to get old.

A couple of days ago I spent about an hour short circuiting this. I’ve set things up so that ssh-agent directly asks for my 1Password master password, uses it to unlock the vault, grab the SSH key password and add the identity to ssh-agent! Read on to know how it works!

I want to call out that this kind of tinkering is only possible because unix tools are customizable and 1Password publishes the full details of its opvault file format. Keep supporting these kind of companies!

We need a couple of things for this setup to work.

  1. Some way to convince ssh-agent to use this mechanism instead of the default password prompt.
  2. Some way to get the SSH key password from 1Password, given the master password.

Customizing ssh-add

While part 2 is the slightly harder part, it is worth spending a few minutes figuring out if 1 is even possible. I certainly am not feeling up to actually hacking on the SSH code. So let’s look at the ssh-add man page:

DISPLAY and SSH_ASKPASS
       If ssh-add needs a passphrase, it will read the passphrase from the current terminal if it was run from a terminal.
       If ssh-add does not have a terminal associated with it but DISPLAY and SSH_ASKPASS are set, it will execute the pro‐
       gram specified by SSH_ASKPASS (by default “ssh-askpass”) and open an X11 window to read the passphrase.  This is
       particularly useful when calling ssh-add from a .xsession or related script.  (Note that on some machines it may be
       necessary to redirect the input from /dev/null to make this work.)

OK, seems like this is possible. It isn’t clear yet how the entered password is read from ssh-askpass. More mansplaining:

Pressing the ‘OK’ button accepts the pass-phrase (even if it is empty), which
is printed on the standard output, and the dialog exits with a status of zero
(success). Pressing the ‘Cancel’ button discards the pass-phrase, and the
dialog exits with non-zero status.

Very unix-y. The program just needs to write the password to stdout. OK. Let’s come back to this once we have a script doing exactly that.

Extracting passwords from 1Password

The opvault file format is open and well documented. This means we don’t have to figure out some complicated IPC schemes or reverse-engineering. There are already libraries out there that support parsing these files. While writing one in Rust would be the cool thing to do, I’m trying not to fall too deep in the XKCD trap. I picked the opvault python package. I did a quick read of the code to make sure this wasn’t secretly uploading all my passwords to the Internet. I also used virtualenv and some extra customizations to not pollute my system python, but I’m going to elide that. The code presented here assumes your system python has opvault installed. Remember, the script also needs to be executable.

#!/usr/bin/env python
import os
from getpass import getpass

from opvault.onepass import OnePass

def main(path):
    master_pass = getpass('master password: ')
    vault = OnePass(path=path)
    vault.unlock(master_password=master_pass)
    vault.load_items()
    matched = vault.get_item('pixelbook ed25519 ssh key')
    assert len(matched) == 1
    _, details = matched[0]
    print(details['password'])

if __name__ == '__main__':
    main(os.path.expanduser('~/1Password.opvault'))

This is a fairly simple script. It hard-codes the location to my vault and the title under which my key is stored in 1Password (one less thing to worry about passing around on the command line). It uses the getpass module to retrieve my password in the unix-style, without echoing it on screen. We load the vault, load all the items and retrieve the details. Then we print the SSH key password!

Plugging this into ssh-add

This may vary slightly based on how you’ve set up ssh-add to execute at startup. I use zprezto, and I’ve the ssh module enabled. I elected to put the customization in my .zshrc, right before initializing zprezto.

_old_display="$DISPLAY"
_old_askpass="$SSH_ASKPASS"
export DISPLAY=foobar SSH_ASKPASS="$HOME/1pass-askpass.py" 
# Source Prezto.
if [[ -s "${ZDOTDIR:-$HOME}/.zprezto/init.zsh" ]]; then
  source "${ZDOTDIR:-$HOME}/.zprezto/init.zsh"
fi

export DISPLAY="$_old_display" SSH_ASKPASS="$_old_askpass"
unset _old_display _old_askpass

I’m not entirely happy with this, but it will do for now. First, it changes these variables at the zprezto level, instead of just at the module level. This means other modules and zsh setup can be influenced by this. However, putting these links right before loading ssh did not seem to work and I don’t care enough. Second, I’m not resetting the variables properly, they now end up as empty strings instead of being unset if they were not set before. Again, I don’t care.

That’s it! Use ssh-add -d to disassociate the existing identity, then start a shell to see if this works, as I did several times while figuring this out.

A note on the vault storage.

This deals only with local vaults. If you use the 1Password web service, I’d be happy to know how you would hook that up. If you use Dropbox to sync your 1Password vaults, the easiest way to get this to work is by installing Dropbox on your linux machine and just syncing the files. I thought this was a lot of resource use just to access this SSH key. In particular, the SSH key password is never going to change, so the syncing aspect is not very useful. Instead, I just downloaded a current version of the opvault file (which is actually a directory). I also pruned the contents to leave only the profile and the band with the ssh key on-disk, as those are the only pieces required.