pass
and git over network with expiring keypairs
I have been using the unix cli tool as a password manager pass
for a while now.
The main benefit of using it is its minimalistic nature, and you can access it directly from the command line.
All of the passwords are simply saved as gpg-encrypted text files, and you can perform bash scripting with them, if you wish.
In addition, you can pipe the output of pass
directly to something like xclip
to add it to your clipboard.
Recently, I decided it was time to upgrade my gpg key since it was generated and unchanged since 2019. That keypair was still using rsa2048
, but currently (2024) rsa4096
is more common. Also there is no way to take a single key and upgrade it to a higher bit factor. I was initially not sure if there was a convenient way to transition my current pass
configuration to the new gpg key. The man page for pass
says that pass init
has some function to re-encrypt your passwords with a different set of gpg keys, but this was either unintuitive or not working for me. So I instead accomplished that part with a bash script.
One additional experience I had difficulty with in using pass
was in simultaneously using my data on multiple different machines. My initial solution was to simply transfer the ~/.gnupg/
and ~/.password-store/
directories to each machine, but then I experienced some issues with keeping them all asynchronously congruent while still trying to stay secure.
I didn't find too many helpful guides on this process. But I came up with the following workflow, and I found it quite elegant.
I use a master gpg key on a "locked-down" device, and only provide expiring subkeys to my personal devices. This gives an extra layer of protection, if any of those devices are compromised, I can simply regenerate new subkeys to use with my password store. Because the subkeys expire, I do need to repeat some steps from this guide on a periodic basis.
In addition, I am hosting a git repository on this locked-down device, and any changes I make can be done as regular git pushes and pulls with gpg signature authentication. But I also configured the git remote to disallow any force push
es, only appending new changes if the user can sign the commit. Note of course I am not hosting the git repo on a public network like GitHub. This required some different action than you may already be used to.
Firstly, I would recommend backing up any current gpg and pass configuration.
cp -r ~/.gnupg ~/.gnupg.bkp
cp -r ~/.password-store ~/.password-store.bkp
If you don't already have a gpg keypair, or want to create a new one, run.
gpg --full-generate-key
At any point, you can list your current private keys with the subkey IDs using the following command:
gpg --list-secret-keys --keyid-format LONG
My new key has a response that looks something like the following:
sec rsa4096/7D4DXXXXXXXXXXXX 2024-11-28 [SC]
0DEFXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
uid [ultimate] myname
ssb rsa4096/6F24XXXXXXXXXXXX 2024-11-28 [E]
By default, it looks like the master key might already include some subkeys. We will generate new ones with an expiration date to distribute to other devices. One should be for signing and the other for encryption. Do this by editing the master key and running addkey
in the gpg shell that spawns.
gpg --edit-key 0DEFXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
> addkey
Once you list the secret keys again, you should now also get something that looks like the following. Note the created date, expiry date, and usage of each key.
ssb rsa4096/8D9FXXXXXXXXXXXX
created: 2024-11-28 expires: 2025-11-28 usage: S
ssb rsa4096/894BXXXXXXXXXXXX
created: 2024-11-28 expires: 2025-11-28 usage: E
I opted to use a different password on my personal devices since I wanted the many characters for my master key but don't want to have to type that every time. There is less security in using fewer characters for these subkeys, but in this way at least I can revoke the subkeys whenever I want while still maintaining a secure key for my identity.
It appears impossible on the same machine to have different passwords for the master and subkeys unless you re-import the subkeys into a different directory.
Export your subkeys in the following manner, don't forget the exclamation mark:
gpg --export-secret-subkeys 8D9FXXXXXXXXXXXX! > signing_subkey.priv
gpg --export-secret-subkeys 894BXXXXXXXXXXXX! > encryption_subkey.priv
Now copy these over to your personal PC/mobile device(s) and import them using the master key password.
gpg --import signing_subkey.priv
gpg --import encryption_subkey.priv
Now set the password to something shorter.
gpg --edit-key 0DEFXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
> passwd
Enter the master key's true password, and then enter your new local password. It will overwrite the passwords for the local keys, but not the master key.
Export the keys with the shorter password, this is what you will share on other devices
gpg --export-secret-subkeys 8D9FXXXXXXXXXXXX! > signing_subkey_shortpass.priv
gpg --export-secret-subkeys 894BXXXXXXXXXXXX! > encryption_subkey_shortpass.priv
During the following setps, make sure your gpg_agent.conf has at least the following:
pinentry-program <...>
default-cache-ttl 3600
max-cache-ttl 3600
This will ensure your password is kept in memory for some time after you enter it. Use the pinentry program that is most relevant to you, possibly the ncurses one or a GUI one. If you experience a failure in one of the following commands, it might be because you are not currently authenticated through pinentry and that can't happen in the command's subshell.
Set up GPG signing for git locally on this server device as well as any personal devices you want to access passwords from. This is required to push changes to your passwords. You may also want to pass the --global
flag to apply to other git repos.
git config user.signingkey 8D9FXXXXXXXXXXXX
Enable gpg signing for commits. Again consider --global
git config commit.gpgsign true
If you do not have one already, create password-store git repository for your private keys. I think you should NOT include an exclamation mark after the subkey here
pass init 0DEFXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 894BXXXXXXXXXXXX
pass git init
If you want this machine to be a git remote for my mobile devices, we need to ensure the host machine's git repo only allows GPG-verified clients to append to the repo. This requires each commit to be signed.
Now add the following script as a pre-receive hook
cd ~/.password-store
vim .git/hooks/pre-receive
#!/bin/bash
# List of acceptable GPG key IDs (or subkeys) that can sign commits
REQUIRED_GPG_KEYS=(
#0DEFXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # I comment this out for maximum safety
8D9FXXXXXXXXXXXX # The current non-expired subkey
# Add more key IDs or subkey IDs as needed
)
while read oldrev newrev refname; do
# Prevent any force push (i.e., the newrev is not an ancestor of oldrev)
if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then # This allows the first commit to go through, especially for new branches.
if ! git merge-base --is-ancestor $oldrev $newrev > /dev/null; then
echo "ERROR: Force push detected! Push rejected."
exit 1
fi
fi
# Check each commit in the push
if ! git rev-list $newrev | while read commit; do
# Get the GPG key ID used to sign the commit
signer=$(git show --no-patch --no-notes --pretty='%GK' $commit)
# Check if any of the required GPG key IDs match the signer
match_found=false
for key in "${REQUIRED_GPG_KEYS[@]}"; do
echo "Current signer: $signer"
echo "Required key: $key"
if [[ "$signer" == *"$key"* ]]; then
match_found=true
break
fi
done
# If no match was found, reject the push
if [[ "$match_found" == false ]]; then
echo "ERROR: Commit $commit is not signed with an allowed GPG key."
exit 1
fi
done; then
echo "Push rejected: Some commits are not signed with an allowed GPG key."
exit 1
fi
done
Also make sure the hook is executable.
chmod +x .git/hooks/pre-receive
I would recommend committing this file to your git repo as well in case you might lose it. Run the following command and hit enter to confirm the commit.
cp .git/hooks/pre-receive ./pre-receive.hook && git add pre-receive.hook && git status && read && git commit -m "Added a copy of the server's pre-receive hook to the repository to track any changes." && git log
If for some reason at any point you need to reapply signatures to your git commit history (perhaps some were missing before you adjust the pre-receive hook shortly), you can run the following command to go and re-approve all of them.
git rebase --exec 'git commit --amend --no-edit -S' -i --root
Update passwords to the new key:
NEWKEY_SUB="894BXXXXXXXXXXXX" find . -type f -name "*.gpg" | while read -r file ; do plaintext="$(gpg --quiet --batch --yes --decrypt "$file")" && echo "$plaintext" | gpg --encrypt --quiet --batch --yes -r ${NEWKEY_SUB}! -o "$file" && echo "Re-encrypted: $file" || echo "Failed to encrypt: $file" >&2 ; done
Test the updated password hashes with a regular pass command.
pass path/to/some/password.gpg
Now commit those to the .password-store
git repo on your server.
Because git by default prevents collaborators from overwriting files on your system while the repo is checked out, you need to set the following configuration option to automatically trigger the server's local checked-out files to be overwritten when a push comes in. Otherwise you could on this server run git checkout --detach
to allow commits from other machines and then git checkout main
to update the contents of your directory after such a commit.
git config receive.denyCurrentBranch updateInstead
Now, connecting via ssh, clone the git repo onto a personal device and enable signing as you did for the server.
git clone ssh://user@192.168.0.1XX:22/home/user/.password-store
You could test the pre-release hook with a simple test commit.
touch a && git add a && git status && read && git commit -m "test commit" && git push origin main
Try reverting the commit on your client:
git reset --hard HEAD~1 && git clean -fd && git push --force origin main
And actually succeed in doing so on the server
git reset --hard HEAD~1 && git clean -fd
For extra good measure, on a new device try cloning the repo and committing something else without first importing gpg keys. In fact, do so by cloning the repo from your more-unsecure personal device.
git clone ssh://user@192.168.0.1YY:22/home/user/.password-store
...
Finally, when you're ready, clean up and delete any old files you don't want to keep around. I use shred to prevent people from recovering the data off my drive post-deletion. Note the following commands could be dangerous if you don't modify carefully (watch those *
s!).
shred -u signing_subkey.priv encryption_subkey.priv
shred -u signing_subkey_shortpass.priv encryption_subkey_shortpass.priv
sudo for i in .password-store.bkp .gnupg.bkp ; do find "$i" -type f -exec shred -u -v {} \; && find "$i" -type d -empty -exec rmdir -v {} \; ; done
Also you can revoke or delete keys from your keyring, if you find that helpful.