← Recipes

阅读中文版本

Encrypt Secrets in Git with SOPS and age

Store encrypted secrets beside your configs with SOPS and age, keep decryption keys outside the repo, and rotate without re encrypting everything.

~25 minsecuritysecretsgit

Task

Move plaintext secrets out of the repository and into encrypted files that only environments with the right key can read.

When to use this

Deploy keys and API tokens sit in a private repo. The repo went from private to shared-with-vendors, and nobody could enumerate what secrets it contained.

Prerequisites

  • SOPS and age installed locally and in CI
  • A key pair per environment, private keys delivered to their environments only
  • Secret files currently in plaintext to migrate

Steps

1. Generate per-environment key pairs.

age-keygen -o keys/staging.txt && age-keygen -o keys/prod.txt && cat keys/staging.txt | grep -o "age1[^ ]*" | head -1

Expected output: Each environment has its own key pair; only the public parts ever enter the repository.

2. Encrypt the secrets file for the environments that need it.

sops --encrypt --age age1stagingpub,age1prodpub --in-place config/secrets.yaml

Expected output: The file becomes encrypted YAML with readable keys and ciphertext values, diffable and mergeable in git.

3. Verify plaintext is gone from the tree and history going forward.

git grep -l "api_key" -- config/ ; sops --decrypt --output /dev/null config/secrets.yaml && echo "decryptable with local key"

Expected output: No plaintext secret files remain, and the local machine can decrypt because it holds the staging private key.

4. Decrypt in CI from an injected key.

- name: Deploy with secrets
  env:
    SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY_FILE }}
  run: |
    sops --decrypt config/secrets.yaml > /tmp/secrets.yaml
    ./deploy.sh /tmp/secrets.yaml

Expected output: CI decrypts only with the environment key injected as a secret, never stored in the repo.

5. Rotate a compromised environment key without re-encrypting values.

sops --rotate --in-place config/secrets.yaml

Expected output: SOPS re-encrypts the data keys under the new recipient list; the underlying secrets are untouched and no one re-enters values.

Troubleshooting

Problem

Cannot decrypt after cloning on a new machine

Solution

The private key never left the original machine; distribute environment private keys through your secret-delivery process, not through the repo.

Problem

CI decryption fails with keyring errors

Solution

The SOPS_AGE_KEY env var must contain the private key file contents; confirm the workflow secret stores the full file, newlines included.

Problem

Merged conflicts in encrypted files

Solution

Per-key encryption keeps structure readable; resolve conflicts by decrypting both sides, merging the plaintext maps, and re-encrypting the result.

Limitations

  • Encrypted-at-rest in git still requires the private keys to be guarded; the repo was never the safe for keys, only for data.
  • SOPS protects values, not secret names or structure; those remain visible by design for reviewability.

The repo is for structure, the key is for value

A repository is a fine place for the shape of your configuration and a terrible place for its secrets. SOPS with age splits the two: files stay in git with readable keys and encrypted values, so reviews see what changed structurally while only environments holding the right private key see the values. Rotation re-wraps data keys in place, and the plaintext never returns to the working tree.

# Encrypt for the environments that may read it
sops --encrypt --age age1stagingpub,age1prodpub \
  --in-place config/secrets.yaml

# CI decrypts with an injected private key
SOPS_AGE_KEY="$KEY_FILE" sops --decrypt config/secrets.yaml

One secret, many recipients

SOPS encrypts each file data key to multiple age recipients, so staging and production read the same file while development cannot. The recipient list is itself reviewable in the file metadata: the pull request that adds a vendor public key is a visible, discussable security event rather than a quiet permission change.

Rotation without re-entry

The expensive part of secret rotation used to be re-encrypting every value by hand. With SOPS the values are encrypted under a per-file data key, and rotating means re-wrapping that key under new recipients, one command, zero re-typing. That cheapness is what makes quarterly rotation actually happen instead of being postponed forever.