skip to content

Search

Securely exporting API keys as environment variables

3 min read

Learn how to securely manage API tokens in your shell using pass, replacing hardcoded values in .zshenv with encrypted lookups.

In my $HOME, there’s a single .zshenv file, which is not under source control in my dotfiles repo. This file strictly exports only the tokens and API keys specific to the machine.

It looks something like this:

source ~/.config/zsh/.zshenv
 
# tokens
export GITHUB_TOKEN="ghp_..."
export GITLAB_TOKEN="glpat-..."
export NPM_TOKEN="npm_..."
 
# API keys
export OPENAI_API_KEY="sk-proj-..."

The Problem

I’ve always felt this is breaching a lot of security practices: imagine accidentally running cat ~/.zshenv in front of others, or leaving your laptop unlocked while away.

The Solution

I came up with a fairly simple solution that adds minimal setup and has worked reliably for over 3 months.

The solution is to use pass to create GPG-encrypted password files. Here’s a snippet to get started:

# Install pass
brew install pass
 
# You can control where password files are saved with this variable:
export PASSWORD_STORE_DIR="$XDG_DATA_HOME/pass"
 
# Initialise pass with a new or existing GPG key.
# This key is used for password (en)decryption.
# This command creates `$PASSWORD_STORE_DIR/.gpg-id` file.
pass init <GPG user ID or key ID>
 
# Create a password file
pass edit <pass-name>

What I like about pass is that it follows the UNIX philosophy of using the file system for everything. The secrets remain encrypted at rest, and are only decrypted in-memory when accessed with pass show. This means you can safely put the entire password folder under version control and sync it across machines (I do this), as long as the GPG private key is not committed or shared. Password names are literally paths to their corresponding .gpg files, which lets me organise my passwords like a project directory:

$ tree ~/.password-store
.password-store
├── api
   └── openai
       └── cowboy-bebug.gpg
├── token
   ├── github
   └── cowboy-bebug.gpg
   ├── gitlab
   └── cowboy-bebug.gpg
   └── npm
       └── @your-org.gpg
└── .gpg-id

After creating passwords, the .zshenv file can be updated like below to remove all hardcoded tokens and keys:

source ~/.config/zsh/.zshenv
 
# tokens
export GITHUB_TOKEN=$(pass show token/github/cowboy-bebug)
export GITLAB_TOKEN=$(pass show token/gitlab/cowboy-bebug)
export NPM_TOKEN=$(pass show token/npm/@your-org)
 
# API keys
export OPENAI_API_KEY=$(pass show api/openai/cowboy-bebug)

Extra Security

If I leave my laptop unlocked, an attacker could still run pass show in a terminal.

To prevent this, we can just add a passphrase for the GPG key used by pass:

# This will trigger gpg prompt:
gpg --edit-key YOUR_KEY_ID
 
# Inside gpg prompt:
gpg> passwd
 
# After setting/changing passphrase:
gpg> save

Now, you’ll be prompted for the passphrase each time you open a new terminal session (or when gpg-agent requires it). You can control how long the passphrase is cached by creating gpg-agent.conf file like this:

default-cache-ttl 600  # cache passphrase for 10 minutes after each use
max-cache-ttl 3600     # absolute max time (1 hour) to keep it cached