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.
- Some way to convince ssh-agent to use this mechanism instead of the default password prompt.
- 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.