Using SOPS with Age and Git like a Pro

We have some shared secrets we need to set up our infrastructure. Those shared secrets are (currently) mainly technical accounts (like our CI/CD systems and other build tools).

age - A simple, modern, and secure encryption tool (and Go library) with small explicit keys, no config options, and UNIX-style composability.

In this example, we use age to encrypt sensitive files and values required to set up the infrastructure.

Side-note: Since we missed this detail for a long time in our project, please note: The author pronounces it [aɡe̞], like the Italian “aghe”.

Prepare the Basic SOPS Setup

Let us kick-start the workspace from Alice and Bob, who want to share a repo with credentials.

git init workspace-Alice
git init workspace-Bob

They decided to use age encryption to store the development secrets in the shared repository.

mkdir secrets
age-keygen -o secrets/age-key.txt

Since this is the private key, Alice uses .gitignore to prevent accidentally publishing the key.

echo 'secrets/age-key.txt' >> .gitignore

Both generated public keys are stored in public-age-keys.txt as potential recipients for the encryption.

echo 'age1ls2ftwdzyu0ptdqe8mysde7mzwjdagqvvepr3hk2ysduhlz47enqxtymfh,age17aeun7qfjz470e4k4r3650h6hxdghtnh3z73llczzzzhvhlal44q4h8lsq' > public-age-keys.txt

Check the Setup with the sops CLI

Alice uses a secret.json locally...

echo -n '{
  "password": "42"
}' > secret.json

and stores the encrypted version in the repo manually:

# Alice
export SOPS_AGE_RECIPIENTS=$(<public-age-keys.txt)
sops --encrypt --age ${SOPS_AGE_RECIPIENTS} secret.json > secret.json.enc
git add secret.json.enc
git commit -m "Add encrypted shared secret"

With his key added to the list of recipients, Bob can manually decode and use the secret locally:

# Bob
export SOPS_AGE_KEY_FILE=$(pwd)/secrets/age-key.txt
sops --decrypt --input-type json --output-type json secret.json.enc > secret.json

Capture the Commands in a Script

Bod decided to capture the encryption and decryption steps in shell scripts to kick-start the automation (with git). The basic idea is to reuse the scripts in clean and smudge filters with Git later...

mkdir scripts

The encrypt.sh script...

echo -n '#!/bin/bash

scriptDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd "${scriptDir}/.." || exit 1

export SOPS_AGE_RECIPIENTS=$(<public-age-keys.txt)
exec 3<<< "$(cat $1)"
sops --encrypt --input-type json --output-type json --age ${SOPS_AGE_RECIPIENTS} --encrypted-regex "^(user|password)$" /dev/fd/3
' >> scripts/encrypt.sh
chmod u+x scripts/encrypt.sh

and decrypt.sh script:

echo -n '#!/bin/bash

scriptDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd "${scriptDir}/.." || exit 1

export SOPS_AGE_KEY_FILE=$(pwd)/secrets/age-key.txt
exec 3<<< "$(cat $1)"
sops --decrypt --input-type json --output-type json /dev/fd/3
' >> scripts/decrypt.sh
chmod u+x scripts/decrypt.sh

Bob adds both scripts to the shared repository:

# Bob
git add scripts
git commit -m "Add SOPS encryption/decryption scripts"

Test Encryption and Decryption Scripts

Bob adds the latest changes to secret.json, encrypts the file with his script, and pushes the result...

# Bob
./scripts/encrypt.sh secret.json > secret.json.enc
git add secret.json.enc
git commit -m "Update encrypted shared secret"

Alice can use the provided script to easily decrypt the file without having to remember the command-line options:

# Alice
./scripts/decrypt.sh secret.json.enc > secret.json

Git Filters - Automagically SOPS All The Things

With the scripts provided, Alice is equipped with the last missing pieces to automate the overall process. Instead of semi-manually decrypting the file before using a Git filter can do the trick:

# Alice
git config --local filter.sops.smudge $(pwd)/scripts/decrypt.sh
git config --local filter.sops.clean $(pwd)/scripts/encrypt.sh
git config --local filter.sops.required true

Check the freshly created section in .git/config (optional):

grep -A 3 sops .git/config

Enable the filter for the plain file...

echo 'secret.json filter=sops' > .gitattributes
git add .gitattributes
git commit -vm 'Hook secret.json into Git Filter sops'

and finally, replace the encrypted file:

# Alice
git add secret.json
git rm secret.json.enc
git commit -vm 'Switch to Git Filter for shared secret'

To opt in into the cool new magic world, Bob configures his Git alike:

# Bob
git config --local filter.sops.smudge $(pwd)/scripts/decrypt.sh
git config --local filter.sops.clean $(pwd)/scripts/encrypt.sh
git config --local filter.sops.required true
git pull

Tip: Run as if git was started in path: git -C <path>

This option allows for operating with both repositories within a single terminal.

git -C workspace-Alice git push
git -C workspace-Bob git pull

If unsure, you can inspect the shared secret in your local git-workspace with the following command:

GIT_HASH=$(git rev-list --objects -g --no-walk --all | grep secret.json | cut -d ' ' -f 1)
git cat-file -p ${GIT_HASH}

Eves Session - just for the record

Without a proper age key, the session from Yves will lead to nothing but encrypted values...

git clone repository
cd repository

shows the encrypted shared secret:

{
  "password": "ENC[AES256_GCM,data:09GJdw==,iv:nWF898ZCjkwjiYQSEQiTpxXeZwkD+Cn0Z9PJlgdEJ0Q=,tag:jhrwmqLObZdA6wyu7o+odA==,type:str]",
  "sops": {
  }
}

TODO - Explore usage of .sops.yaml

This could be a nice alternative to store for public age keys and configuration

echo -n 'creation_rules:
  - shamir_threshold: 1
    path_regex: "secret.json"
    encrypted_regex: "^(user|password)$"
    key_groups:
      - age:
        - age1t2c8jft25k5nnr7m2zln473dkxegwvx5ge2pfgarfnaepepmzpzszz63qy
' >> workspace/.sops.yaml

Photo by James Coleman on Unsplash