sops
SOPS encrypts values while leaving keys readable. Git diffs show which field changed without exposing secrets. Uses age, GPG, or cloud KMS.
Setup
sudo pacman -S sops
export SOPS_AGE_KEY_FILE="$HOME/.secrets/.metadata/keys/master.age.key"
.sops.yaml in repo rootcreation_rules:
- path_regex: data/.*\.sops\.yaml$
age: >-
age1wtdeuelfua4afrqqtw8claqf5wc335g7euhgh22pjzd57azpgq3q7jqcnn
The .sops.yaml only contains the public key — safe to commit.
Encrypt
cat > data/d000/config.sops.yaml << 'EOF'
database:
host: db.example.com
username: admin
password: supersecret123
EOF
sops --encrypt --in-place data/d000/config.sops.yaml
database:
host: ENC[AES256_GCM,data:...,type:str]
username: ENC[AES256_GCM,data:...,type:str]
password: ENC[AES256_GCM,data:...,type:str]
sops:
age:
- recipient: age1wtd...
lastmodified: "2026-05-02T..."
Decrypt
sops --decrypt data/d000/config.sops.yaml
sops --decrypt --extract '["database"]["password"]' data/d000/config.sops.yaml
export DB_PASS="$(sops --decrypt --extract '["database"]["password"]' data/d000/config.sops.yaml)"
Edit
sops data/d000/config.sops.yaml
No manual decrypt/re-encrypt cycle. Just edit and quit.
age vs gopass vs sops
| Tool | Best for | How it encrypts |
|---|---|---|
|
Full files — prose, documents, recordings, AsciiDoc |
Entire file → opaque binary blob. No readable structure. |
|
Passwords and credentials with clipboard integration |
GPG/age store. Interactive. |
|
Config files in git — YAML, JSON, ENV |
Values only — keys stay plaintext. Git diffs are readable. |
Decision rule: If you need to git diff the file and see what changed without decrypting — use sops. If the entire file is sensitive (legal docs, recordings) — use age. If it’s a password you access interactively — use gopass.
Configuration — .sops.yaml
Multiple rules (different keys for different paths)
creation_rules:
# Personal files — age key
- path_regex: data/d000/.*\.sops\.yaml$
age: >-
age1wtdeuelfua4afrqqtw8claqf5wc335g7euhgh22pjzd57azpgq3q7jqcnn
# Work files — different key or KMS
- path_regex: data/d001/.*\.sops\.yaml$
age: >-
age1differentkeyhere...
Rules are matched top-to-bottom. First match wins.
Unencrypted keys
By default sops encrypts all values. To leave specific keys unencrypted:
creation_rules:
- path_regex: data/.*\.sops\.yaml$
age: >-
age1wtdeuelfua4afrqqtw8claqf5wc335g7euhgh22pjzd57azpgq3q7jqcnn
unencrypted_suffix: _unencrypted
Then in your file:
config:
host_unencrypted: db.example.com # stays plaintext
password: supersecret123 # gets encrypted
Troubleshooting
# "failed to load age identities"
# → SOPS_AGE_KEY_FILE points to wrong path
echo $SOPS_AGE_KEY_FILE
ls -la $SOPS_AGE_KEY_FILE
# "no matching creation rules"
# → file path doesn't match path_regex in .sops.yaml
# → check: does your filename end in .sops.yaml?
cat .sops.yaml
# Re-encrypt with updated .sops.yaml rules
sops updatekeys data/d000/config.sops.yaml
# Rotate data key (re-encrypts data key, not values)
sops --rotate --in-place data/d000/config.sops.yaml