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