sops Encryption/Decryption Workflow in Git
- The user creates a file called
secrets.env
anywhere in the repository. This file is dotenv-formatted as simple key-value pairs where the value is a secret to be passed into a Stack or container. We want to be able to read the "shape" of the file (the names of the keys), even when the file is encrypted.
- The
.gitattributes
file assigns the following properties to files called secrets.env
:
filter=sops
which tells the local git instance to run the "sops" filter configured in .git/config
.
diff=sops
which tells the local git instance to run the "sops" diff command instead of the normal git diff
when diffing secrets.env
files.
- The
.git/config
is configured with the "sops" filter and diff blocks referenced above.
filter.sops.smudge=
will run the thing after the =
when checking out the repository. This should decrypt our secrets, making them "dirty" again. We call decrypt-filter.sh
and pass %f
as a positional argument, which renders to the path of the file being filtered, relative to the repo root (e.g. homelab/stacks/books/secrets.env
).
filter.sops.clean=
will run the thing after the =
when staging changes to commit. This should encrypt our secrets, making them "clean". We call encrypt-filter.sh
and pass %f
as a positional argument, which renders to the path of the file being filtered, relative to the repo root (e.g. homelab/stacks/books/secrets.env
).
filter.sops.required=true
will ensure that if the scripts fail, the commit will error out.
diff.sops.textconv=
will use the command after the =
instead of git diff
when comparing files. This affects whether files are considered "modified".
- When the user stages a new
secrets.env
file at homelab/stacks/books/secrets.env
, we automatically run encrypt.sh homelab/stacks/books/secrets.env
. This implictly passes the contents of secrets.env
to the script at /dev/stdin
. The script takes the following steps to generate the encrypted content of the file:
- Assert privatekey existence at
~/.age/key
.
- Determine file extension. For now, we're only working with dotenv-formatted files, but sometimes we also need to support binary files. We use this information to set the
--input-type
parameter.
- Encrypt the file contents. This command infers recipients from
.sops.yaml
. The command uses the privatekey (at ~/.age/key
) of the local user to encrypt the secret. We use Shamir's secret sharing with sops key groups to require at least two of: author key, CI/CD key, and deploy key. Of course, the original privatekey can always decrypt the secret.
- Our new
secrets.env
file is committed to the repository. Encrypted and json-formatted (why not dotenv-formatted?).
- Now we want to deploy our new
homelab/stacks/books
Stack. We'll focus on the sops-related aspects of this process.
- Our CI/CD environment is configured with an age keypair. Additional CI/CD environments will each be configured with their own keypairs.
- Our deploy environments (hosts) are each configured with an age keypair.
- Our CI/CD script is configured to decrypt secrets and export the variables before bringing up the Stack.
List Files Managed by sops
git ls-files |\
git check-attr -a --stdin |\
grep 'filter: sops' |\
cut -d':' -f1