A long time ago I had stumbled upon pass
(website), thought it looked interesting, made a note to take a look at it later, and then forgot about it.
This week I was thinking I need some sort of a command line password manager to store my various secrets and stuff like that. Not passwords as such, but I have a lot of PowerShell code where I need to add API Keys or Entra ID App Registration client secrets etc., and I wanted some secure way of doing it. Until now I was storing these in separate files to the code, and referring to them in my code by sourcing the file with the secret.
To give an example, I would create a file like mysecret.ps1
with the following:
1 |
$apiKey = "xxxxx" |
Then, in the actual code file I’d have a line like this:
1 |
. "/path/to/mysecret.ps1" |
That sources the first file, and now I can use $apiKey
in the code without actually putting it in the code. It’s not super secret coz anyone getting a hold of my machine can figure out the file with the secret from the code, and thus get the secret; but it’s way better than including the secret in my code itself, especially since the code is often pushed to GitHub or put in shared locations at work. I could pat myself on being half-secure, though not fully there. ☺️
This week I decided to change that. I know password managers like BitWarden and 1Password have CLI options, but I didn’t want to use either of them (for one, the BitWarden CLI is quite a hassle to setup; and for another, most of these are work secrets and I didn’t want to mix them with my personal BitWarden). I came across pass
again in some Reddit thread, and this time decided to explore it properly.
(BTW, fun fact that I didn’t know. pass
is created by Jason A. Donenfeld, same person who created WireGuard. He’s got a very minimalistic approach to things, which I like).
The coolest thing about pass
is how it’s very simple and follows the Unix Philosophy (write programs that do one thing and do it well). It stores the secrets by encrypting them with GPG (so it doesn’t reinvent the wheel there), and there’s no concepts of folders or metadata or whatever – things are pretty much free form.
I don’t want to go too much into the details of how to setup pass
, as the official page has all the details (and it’s super simple) but here’s what I did. I started using it on macOS, so let’s start there.
macOS
I created a separate GPG key for use with pass
. So let’s start with that.
First, install GPG if not already installed.
1 |
brew install gpg |
Then generate a key.
1 |
gpg --full-generate-key |
Follow the wizard. I went with the defaults: ECC (sign and encrypt), Curve 25519, does not expire, and filled in the rest of the details.
Next, I initialized pass
using the GPG key I created above.
1 |
pass init "my pass key" |
And that’s pretty much it! Then I can add/ remove/ view secrets…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# adding a secret pass insert appId/0eb79d7c-4cb2-48eb-bf20-a9c91fad17f4 # adding another secret pass insert webhook/automationaccount/runbook # viewing a secret pass appId/0eb79d7c-4cb2-48eb-bf20-a9c91fad17f4 # view all secrets pass # editing a secret pass edit appId/0eb79d7c-4cb2-48eb-bf20-a9c91fad17f4 # removing a secret pass rm appId/0eb79d7c-4cb2-48eb-bf20-a9c91fad17f4 # adding another secret pass insert serviceAccounts/exchange/svc_newaccount01 |
As you can see it’s pretty free form. I stored an app Id called “appId/0eb79d7c-4cb2-48eb-bf20-a9c91fad17f4” under a “folder” called “appId”. But there’s nothing folderish about it… it’s just a free form place I chose to put it so things are a bit organized. I could have very well put the secret in the top level itself, or even move it after creating to a different folder.
Viewing a secret is super simple too – just pass
followed by the path to the secret.
(See the official website for some tips on adding more metadata to the secret apart from just the password).
If the GPG passphrase is needed, pinentry usually pops up.
All these secrets are stored in a folder called .password-store
in the home folder. Easy peasy. Each secret is a file of its own, and the “folders” you put them in when creating are also present in the file system.
Using any of these secrets from PowerShell is easy. Simply do:
1 2 |
$appId = "0eb79d7c-4cb2-48eb-bf20-a9c91fad17f4" $clientSecret = Invoke-Command -ScriptBlock { pass appId/${appId} } |
Windows
I wanted to have this setup on Windows too. There is no pass
for Windows natively, so one must use WSL.
In WSL (Ubuntu) I installed pass
.
1 |
sudo apt update && sudo apt install pass |
I then exported the GPG key from macOS to here.
1 2 3 4 5 6 |
# macOS # view all the private keys gpg --list-secret-keys # export the one I want, used by pass gpg --export-secret-keys --armor PASS-KEY-ID |
I was pretty basic and had the Windows machine opened via RDP, so I just copied the key that was output on macOS, and pasted it into a new file (called pass-private-key.asc
in this case) in Windows. Then I did:
1 2 3 4 5 6 7 8 9 |
# windows gpg --import pass-private-key.asc # not needed I think, but trust it gpg --edit-key PASS-KEY-ID Type: trust Select: 5 (ultimate trust) Confirm: Type quit and press Enter. |
Continuing to be pretty basic, I then zipped the .password-store
folder on macOS and copied it over to Windows and extracted it in WSL. And that’s it, I now had the same password on Windows too.
Using pass
in Windows is straight forward. Just add wsl
before the command.
1 2 3 4 5 6 7 8 9 |
# Adding a secret wsl pass insert appId/0eb79d7c-4cb2-48eb-bf20-a9c91fad17f4 # Viewing a secret wsl pass appId/0eb79d7c-4cb2-48eb-bf20-a9c91fad17f4 # Via PowerShell $appId = "0eb79d7c-4cb2-48eb-bf20-a9c91fad17f4" $clientSecret = Invoke-Command -ScriptBlock { wsl pass appId/${appId} } |
I like to copy paste my code between macOS and Windows, and I didn’t want to keep adding wsl
on the Windows side. So I made a batch file called pass.bat
and put it in my PATH
. (Thanks to ChatGPT for this idea actually! 🤖)
1 2 |
@echo off wsl pass %* |
Now I can use the same command between both OSes and code.
macOS again
OK, so how can I keep the passwords in sync between both? My initial idea was about write some code to zip it on the macOS side. I created the following function and added it to my .bash_profile
.
1 2 3 4 5 6 7 8 9 10 11 |
function passCopy { local zipfile="$HOME/Downloads/pass.zip" # Remove existing ZIP file if it exists rm -f "$zipfile" cd $HOME/dotfiles && zip -r "$zipfile" .password-store/ && cd $HOME && osascript -e "set the clipboard to POSIX file \"$zipfile\"" # I can't delete the file here coz macOS copies only a reference to the file, not the actual file contents. # When you delete the file, the clipboard loses access to it. } |
This zips the folder and puts it in my Downloads folder and also copies it to the clipboard. ☺️
Windows again
And on the Windows side I added this to .bash_profile
in WSL.
1 2 3 4 5 6 7 8 9 10 11 |
function passExtract { local file_path="$HOME/pass.zip" local folder_path="$HOME/.password-store" if [ -f "$file_path" ]; then rm -rf "$folder_path" unzip "$file_path" rm -f "$file_path" else echo "File $file_path does not exist. No action taken." fi } |
So all I do now after making some changes in macOS (which is where I mostly work in), I type passCopy
in the terminal (in Bash). Switch to Windows, go to the WSL folder in File Explorer, press Ctrl+V
to paste the file, and in WSL terminal I type passExtract
.
Not a 100% ideal but better than nothing!
macOS one more time!
Then I realized pass
can integration with git. It was there in the man page but I missed that initially. Wow!
So all I do is:
1 2 3 4 5 6 7 8 9 10 11 12 |
# initialize git # this will create a .git folder in .password-store and add all existing secrets as a commit pass git init # add remote pass git remote add origin github:pass-store # pushing, first time pass git push -u --all # pushing after any other changes pass git push |
With this in place, all I need to after any changes to pass
is also do pass git push
.
Windows one more time!
So now I can get rid of the copy pasting I was doing earlier. 😃
What I did is do one final copy and paste between the two OSes – this way the .password-store
in Windows too is Git initialized. (I did this also coz I couldn’t find any instructions on how to initialize a new .password-store
from Git in Windows. I think the steps would be to clone the git repo to .password-store
and then use pass
as usual, but I was too lazy to try).
Now I can add/ remove/ manage secrets in either Windows or macOS. Wherever I make changes I must do pass git push
after making changes; and on the other side do pass git pull
to get it. Sweet!
To make things simpler, I modified the pass.bat
file I created above like this:
1 2 3 |
@echo off wsl pass git pull wsl pass %* |
This pulls the secrets so I always have the latest version. Now I can make a change in macOS, and as long as I didn’t forget to do pass git push
after making changes I should pick it up straight away in Windows/ WSL (update: see 1️⃣ below).
One problem with this though, is that I use SSH to connect to GitHub and it kept prompting me for the passphrase of the SSH key. In WSL or Windows this was a one time affair as ssh-agent
remembers it, but when running from Windows via wsl pass git pull
it kept asking each time.
The fix for that was to add the following in .bash_profile
or .bashrc
in WSL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# thanks to https://superuser.com/a/1828793 ssh_pid=$(pidof ssh-agent) # If the agent is not running, start it, and save the environment to a file if [ "$ssh_pid" = "" ]; then ssh_env="$(ssh-agent -s)" echo "$ssh_env" | head -n 2 | tee ~/.ssh_agent_env > /dev/null fi # Load the environment from the file if [ -f ~/.ssh_agent_env ]; then eval "$(cat ~/.ssh_agent_env)" fi # add all the keys ssh-add -l > /dev/null || ssh-add |
This is needed so the ssh-agent
session is shared across multiple WSL instances.
And I modifed pass.bat
thus:
1 2 3 |
@echo off wsl bash -c 'eval "$(cat ~/.ssh_agent_env)"; pass git pull' wsl pass %* |
This way before pass git pull
runs it also pulls the ssh-agent info and both are run in the same bash session. No more passphrase prompts each time! Yay.
And that’s about it for now. Now I have a super simple CLI based password manager I can use on macOS and Windows for the various code I write, without saving secrets in plain text on either machine. 🤘
Updates:
1️⃣ Created a function like this in .bash_profile
or .bashrc
in macOS. This will push after every operation. Might be an overkill.
1 2 3 4 |
function pass { $(brew --prefix)/bin/pass "$@" $(brew --prefix)/bin/pass git push } |
And this in PowerShell profile.
1 2 3 4 5 6 7 8 9 |
function pass { param( [Parameter(ValueFromRemainingArguments=$true)] [string[]]$args ) & "${Global:BREW_PREFIX}/bin/pass" @args & "${Global:BREW_PREFIX}/bin/pass" git push } |