One thing I’ve always been uneasy about is how many API tokens, passwords, and general login details are stored in a user’s shell config.

Sure, it’s easy. But what’s stopping a seemingly benign program you download from a random repo on the internet from searching your config for GITHUB_TOKEN and exfiltrating the data?

I’ll use the GitHub CLI tool, gh, as my example.

During setup, it generates and stores an access token on your computer.

$ cat ~/.config/gh/hosts.yml

github.com:
    user: nick-f
    oauth_token: <your access token here>

Yikes. Plaintext token in a predictable location that allows access to my GitHub account. Time to fix that.

Yubikey

Firstly, you’ll need a hardware token that can store your GPG key.

Yubikey recently released their 5C NFC token which ticks all the boxes for me. I’ll refer to a Yubikey throughout this, but substitute that with whatever token you have.

GPG key

Next, you’ll need to move your GPG key to the Yubikey.

drduh has a comprehensive guide that will take you through every step. From purchasing your Yubikey, to generating your GPG key, to copying it across to your Yubikey, it’s all there.

I highly recommend using that to guide you.

If you’re wanting to require a physical touch on your Yubikey, ensure that you enable that. Again, drduh has instructions for that.

Storing your tokens securely

pass is a command line password manager that uses your GPG key. Okay, fine, it’s a fancy wrapper around GPG that removes the need for you to change directories. But that’s one less thing for us to worry about.

Once it’s installed and set up, add your token. pass stores your token in a standard GPG format, so you’re never locked into using pass.

I’ve called my entry gh-token, but you can call it whatever you like.

$ pass add gh-token

Enter password for gh-token:
Retype password for gh-token:

Running your command

Thankfully, gh also allows you to pass in your token as a command line variable.

GITHUB_TOKEN=<your token here> gh <commands>

Instead of storing the token in my shell config (that’s what we’re trying to avoid!), I can retrieve it from pass in a subshell.

GITHUB_TOKEN=(pass gh-token) gh <commands>

When retrieving the access token from pass, it decrypts it using your GPG key stored on your Yubikey (authenticated with a PIN and physical touch).

Obviously this is a bit of a pain if you had to type that every time you wanted to use gh. I created an alias in my config to avoid this:

alias gh='GITHUB_TOKEN=(pass gh-token) /usr/local/bin/gh'

Note that I provided the full path to gh.

In Fish shell, it detects an infinite loop if we just reference gh instead of the full path.

Additionally, it ensures that I’m definitely running the program that I want to, rather than relying on PATH precedence to be consistent.

The formatting is slightly different for bash/zsh, but the idea is the same.

alias gh='GITHUB_TOKEN=$(pass gh-token) /usr/local/bin/gh'

“So how is this any better?”

If a process was to try and run pass gh-token it wouldn’t work, due to the Yubikey requiring a physical touch.

The token is no longer being stored in persistent environment variables.

$ gh auth status # after this step, I need to touch the Yubikey
github.com
  ✓ Logged in to github.com as nick-f (GITHUB_TOKEN)
  ✓ Git operations for github.com configured to use https protocol.

$ echo $GITHUB_TOKEN

$

After some initial, one-time setup my GitHub access token is protected and random scripts can no longer search my environment for tokens.

Bonus: notifications

One problem that I found initially was that I wouldn’t know when my Yubikey was awaiting the physical touch. To get around this, I made another alias, this time to wrap pass.

It makes use of terminal-notifier to create a Notification Centre notification whenever pass is invoked with a command that requires my GPG key. Otherwise, just run pass and don’t show the notification.

function pass
  if test $argv[1] = "show"
    terminal-notifier -group "pass-notification" -title "Pass" -message "Awaiting Yubikey confirmation";
    /usr/local/bin/pass $argv;
    terminal-notifier -remove "pass-notification" > /dev/null
  else
    /usr/local/bin/pass $argv
  end
end

The options used for terminal-notifier are all documented in the readme. One thing that isn’t document is the use of /dev/null redirection. This is a workaround to prevent some output when the previous notifications are removed from being displayed.

Homebrew installation

To make things simpler, I created a simple Ruby script which does all the software side of things and made it available as a Homebrew package on a custom tap.

brew tap nick-f/utilities
brew install passta

The source code can be found at nick-f/passta. It will install pass, terminal-notifier, and my passta helper utility.

Wrapping up

Naturally, you’d need to set this up for each utility in a similar way. To me this is a small price to pay for peace of mind.

There’s definitely some limitations with this approach, and not all programs will support this. If you find that your favourite utility doesn’t support command line variables for access token, why not submit a PR to allow that as an option?