Storing secrects in git repositories with age

In my endeavor to rebuild my IT infrastructure (with a Gitops-ish approach in mind) I needed to take a short break to figure out how to securely store secrets inside of my git repository. There are multiple approaches to this and then there is of course also the possibility to not store any secret at all in git, but rather retrieve them at runtime from e.g. Vault. One popular solution when looking for “encrypt git repository” is git-crypt and while I have used it in the past it relies on gpg for encryption, which means I would need to add my private gpg keys to every system.

A more modern and easier to manage alternative to gpg encryption is age and it even allows encryption based on ssh keys, which to me means one less secret to manage. Age is supported by some tools that serve similar goals to git-crypt, like SOPS and agebox and while SOPS is probably a tool worth knowing it is rather on the complex side, since its meant to support complex workflows. For agebox on the other hand I could not really find a flow that would not lead to many unnecessary git changes, since it removes unencrypted/encrypted files from the working directory when encrypting secrets or decrypting stored files. So in the end I decided to rather use age directly and simplify the process by using the git clean/smudge filter.

The setup of this filter is pretty easy, especially since I will reuse my existing ssh keys. For the encryption of files I have added my public ssh key (called to my git repository in the subfolder keys. For decrypting files my private ssh key can remain in my ~ /.ssh dir.

To add the file encryption to my git configuration the following three commands need to be run. In case you want to add this to your “global” ssh config just remove the --local bits.

git config --local filter.ageencrypt.smudge "age -d -i ~/.ssh/id_rsa4096 -"
git config --local filter.ageencrypt.clean "age -R keys/ -a -"
git config --local filter.ageencrypt.required true

In case I would want to encrypt file for multiple recipients (or simply different ssh keys) the smudge filter can be extended to multiple keys by repeating -R keys/

Which files should be encrypted is controlled through the .gitattributes file. For each file that should be encrypted simply add:

file-to-encypt.ext filter=ageencrypt
# example for my autorestic configuration:
.autorestic.yml filter=ageencrypt

And from that moment on whenever the file gets modified, the only thing actually ending up in the git commit is the encrypted content of the file. Sadly this also means that it is no longer as easy to see changes over time, as everything git knows about is the encrypted text. The easiest way to get all files that decrypted that were already checked out before configuring encryption is to force a “re-checkout” by running the following commands:

git stash save
rm .git/index
git checkout HEAD -- "$(git rev-parse --show-toplevel)"
git stash pop