Home

Awesome

git-agecrypt

Git integration usable to store encrypted secrets in the git repository while having the plaintext available in the working tree. An alternative to git-crypt using age instead of GPG.

Do not use this tool unless you understand the security implications. I am by no mean a security expert and this code hasn't been audited. Use at your own risk.

Why should I use this?

Short answer: you probably shouldn't. Before considering this approach, take a look at SOPS and Hashicorp Vault if they are better suited for the problem at hand. They have a clear security advantage over git-agecrypt.

The one use-case where it makes sense to use git-agecrypt instead is when you want to keep some files secret on a (potentially public) git remote, but you need to have the plaintext in the local working tree because you cannot hook into the above tools for your workflow. Being lazy is not an excuse to use this software.

I have written this to have a more portable and easy to set up alternative to git-crypt.

Usage

  1. First setup git-agecrypt integration for a repository:

    $ git-agecrypt init
    

    This command configures the necessary hooks to encrypt and decrypt git objects and to generate clear-text output for git diff, log etc.

  2. Next step is to configure rules to map encryption keys to file paths:

    $ git-agecrypt config add -r "$(cat ~/.ssh/id_ed25519.pub)" -p path/to/secret.1 path/to/secret.2
    

    An arbitrary number of recipients (public keys) and files can be specified using a single command. Keys can be Age keys, ed25519 SSH keys or stubs generated by Age plugins, e.g. for keys stored on Yubikey PIV module. It is enough to have only one secret key to decrypt the files later.

    Configuration is saved to git-agecrypt.toml file inside the root of the repository

  3. After that, edit .gitattributes to actually use these filters. This is currently a manual step.

    path/to/secret.1 filter=git-agecrypt diff=git-agecrypt
    path/to/secret.2 filter=git-agecrypt diff=git-agecrypt
    

    Files can be specified in the same way as for .gitignore but keep in mind that filters are only applied for files, not directories, so that you need to write /secrets/** instead of /secrets/ to encrypt each file under the secrets directory.

  4. Finally, configure the locations of age identities (private keys) which can be used to decrypt files

    $ git-agecrypt config add -i ~/.ssh/id_ed25520
    

    Location of secret keys are stored outside of version control in .git/config to support having them in different location for each checkout.

Behind the scenes

This application hooks into git using smudge clean and textconv filters. Issuing git-agecrypt init adds them to the repository local .git/config:

[filter "git-agecrypt"]
        required = true
        smudge = /path/to/git-agecrypt smudge -f %f
        clean = /path/to/git-agecrypt clean -f %f
[diff "git-agecrypt"]
        textconv = /path/to/git-agecrypt textconv

These filters are assigned to repository files in .gitattributes. When configured, they are being called for each file when touching the index. Encryption is non-deterministic, so each time git status, git add, etc is run a new ciphertext would be generated. To circumvent this, a blake3 hash is calculated for the plaintext and stored under .git/git-agecrypt/ directory. While the hashes stored match with the file contents in the working tree, git-agencrypt loads the previous ciphertext from the index when git asks for it.

Encryption can work without access to private keys (what Age calls identities). In order to pull remote changes of encrypted files or to see plain diff of files, these have to be configured with git-agecrypt config. They are stored in .git/config conforming to standard git config format:

[git-agecrypt "config"]
        identity = /home/vlaci/.ssh/id_ed25519
        identity = ...

Limitations

The following limitations can be easily improved upon, but they are not blockers for my use-case.