Keeping secrets with 1Password's CLI tool

· 1030 words · 5 minute read

I wrote the initial version of my linkding plugin for Newsboat using dotenv to provide my Linkding API key. You just have a .env file in your home directory with a simple “KEY=VALUE” setup. Works fine for not hardcoding your secrets into scripts and reducing the chance you’ll end up adding a credential to version control, but the secrets are sitting around unencrypted.

Now that I have it working at about the same level of reliability as the other plugins in the Newsboat repo I’ll pull out the dependency on dotenv and just tell the would-be consumer “get this credential into an environment variable, hardcode it, or do something else that feels safe to you.”

For myself, I’m weighing a couple of options because I’d like to do a little better than having a bunch of credentials sitting around in the plain.

Over the years I’ve handled this with mutt by combining gpg with mutt’s source config command:

  1. Make a very minimal rc file in mutt’s config syntax that sets your user and password:

           set [email protected]
           set imap_pass=yourimappassword1234
    
  2. Encrypt that file with gpg:

    gpg -r [email protected] –encrypt passwordfile

  3. Use mutt’s source directive in your muttrc to decrypt the file on the fly and read in those two directives:

    source “gpg -d ~/.mutt/passwords.gpg |"

If you have gpg set up correctly, you’ll get a key authentication prompt when you run mutt. The credentials are never stored in the plain on disk, and you’ll get a gpg password prompt every now and then if you have other stuff going on, such as multiple accounts that each need to periodically source that config when you change between them. mutt’s source is just “do whatever is in this file,” same as zsh’s, so I eventually landed on a way to do the same thing in a shell environment:

$(gpg –decrypt ./setenv.sh.gpg)

That just decrypts setenv.sh.gpg and runs the commands inside, which can be just setting a bunch of environment variables in the current shell environment. The data is never decrypted to disk.

That seems like a good general solution that covers a lot of drive-by security scenarios.

In the process of working that out, I wondered about 1Password’s CLI tool. The few times I saw it mentioned the demos were for secrets management, not retrieval, but I took another look at the docs and it does have some interesting provisions for getting your secrets out of 1Password from the CLI.

Basically, when you use the op command’s run argument you can tell it to source in some environment variables from a .env file that uses op’s internal URI scheme to pull credentials out of your vault. An example resource reference looks like this:

op://Personal/Linkding/password

If you put that in an env file with a variable assignment, the op command can source it and pass it along to a shell command. So:

op run –env-file="$HOME/.env” – ~/bin/linkding.rb

… reads from a file that looks something like this:

LINKDING_USER="op://Personal/Linkding/username"
LINKDING_TOKEN="op://Personal/Linkding/credential"
FRESHRSS_USER="op://Personal/FreshRSS/username"
FRESHRSS_PASS="op://Personal/FreshRSS/credential"

… and supplies the linkding script with a value for this line:

token = ENV[‘LINKDING_TOKEN’]

If you’re already auth’d into 1Password and you’ve enabled integration between the CLI tool and the desktop app, then you don’t have to do anything. If you need to re-auth your 1Password instance, you’ll get a biometric prompt. If you’re ssh’d into a remote host where you wouldn’t get the biometric prompt, you can bypass that by setting OP_BIOMETRIC_UNLOCK_ENABLED=false but you’ll need to explicitly auth the 1Password CLI tool from the command line. It doesn’t seem to just fail over to a CLI auth.

So, handled the 1Password way, your .env file could remain unencrypted, but you’d probably be best off aliasing any scripts or commands you run where you want the op tool to interpolate your .env file.

It’s a little more cumbersome but maybe there’s benefit over the long haul from knowing that you just have to keep credentials in 1Password and can reference them elsewhere instead of maintaining an encrypted .env file. It’s also nice to have a biometric prompt or system auth vs. using gpg keys, which require you to have another password on hand (at least until 1Password quits punting on a gpg agent to pair with the ssh one).

For now I’ve only got enough stuff connected this way to use 1Password with FreshRSS and my Linkding plugin. Newsboat allows you to set your RSS service’s password with output from a shell command, so I’ve got that set to:

freshrss-passwordeval “op read op://Personal/FreshRSS/credential”

If I’m auth’d into 1Password, great: It just runs and sets the credential for NewsBoat. If I’m not, great as well: It just runs and tosses up a biometric prompt before proceeding.

I’m also using the env file pattern for Newsboat, for my bookmarking command:

bookmark-cmd “op run –env-file="$HOME/.env" – ~/bin/linkding.rb”

That’s because I don’t want to write my Linkding plugin to require any particular infrastructure. It just wants an environment variable, which is supplied by wrapping the script in the op run command with an –env-file switch. If someone wants to take the script and use another way to get the variable into their environment, or just decide to take their chances and hard-code it because they’re comfortable with that risk, they can. If I ever stop using 1Password I can similarly just figure out a new secrets backend and may be able to avoid rewriting a bunch of utility scripts if I keep using this approach. If I want to use one of my scripts on a host where I don’t have 1Password, well, there are other ways.

Update: I wondered about that mutt setup I have and ended up seeing what would happen if I replaced the whole “load mutt, source a gpg-encrypted file” thing with simple 1Password resource references. It works pretty well. If you’re signed into 1Password, it just works. If you’re not, you get a biometric prompt:

set imap_user=`op read op://Personal/MuttFastmail/imap_user`
set smtp_pass=`op read op://Personal/MuttFastmail/smtp_pass`
set imap_pass=`op read op://Personal/MuttFastmail/imap_pass`

Same caveats as with everything: If I ever end up wanting to use my mutt config on a machine I can’t put 1Password on, I’d need to go back to my old gpg file pattern.