The eternal problem: you need secrets in your repo for deployment, but you can’t commit plaintext credentials. SOPS solves this elegantly by encrypting only the values while leaving keys readable.
Why SOPS?# Traditional approaches:
Environment variables : Work, but no version controlVault : Great, but complex for small teamsAWS Secrets Manager : Vendor lock-in, API calls at runtime.env files in .gitignore : Hope nobody commits themSOPS encrypts secrets in place. You commit encrypted files. CI/CD decrypts at deploy time. Full audit trail in git.
Installation# 1
2
3
4
5
6
7
# macOS
brew install sops
# Linux
curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64
chmod +x sops-v3.8.1.linux.amd64
sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
Basic Usage with Age# Age is the simplest encryption backend:
1
2
3
4
5
# Generate a key
age-keygen -o ~/.sops/key.txt
# Note the public key (age1...)
cat ~/.sops/key.txt | grep "public key"
Create .sops.yaml in your repo root:
1
2
3
creation_rules :
- path_regex : \.enc\.yaml$
age : age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Now encrypt a file:
1
2
# Create secrets.enc.yaml
sops secrets.enc.yaml
SOPS opens your editor. Add secrets in plain YAML:
1
2
3
4
database :
password : super-secret-password
host : db.example.com
api_key : sk-1234567890
Save and close. The file is now encrypted:
1
2
3
4
5
6
7
8
9
10
database :
password : ENC[AES256_GCM,data:hKL...,type:str]
host : ENC[AES256_GCM,data:vB2...,type:str]
api_key : ENC[AES256_GCM,data:sK9...,type:str]
sops :
age :
- recipient : age1ql3z7hjy...
enc : |
-----BEGIN AGE ENCRYPTED FILE-----
...
Keys are visible. Values are encrypted. You can grep for database.password without seeing the secret.
AWS KMS Integration# For teams, KMS is better than age (key rotation, IAM policies):
1
2
3
4
5
6
7
# .sops.yaml
creation_rules :
- path_regex : environments/prod/.*\.enc\.yaml$
kms : arn:aws:kms:us-east-1:123456789:key/abc-123
- path_regex : environments/dev/.*\.enc\.yaml$
kms : arn:aws:kms:us-east-1:123456789:key/dev-456
Different keys for different environments. Prod secrets stay locked down.
Editing Encrypted Files# 1
2
3
4
5
6
7
8
# Edit in place (decrypts, opens editor, re-encrypts on save)
sops secrets.enc.yaml
# Decrypt to stdout
sops -d secrets.enc.yaml
# Decrypt to file (don't commit this!)
sops -d secrets.enc.yaml > secrets.yaml
In CI/CD# GitHub Actions example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
jobs :
deploy :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Install SOPS
run : |
curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64
chmod +x sops-v3.8.1.linux.amd64
sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
- name : Decrypt secrets
env :
SOPS_AGE_KEY : ${{ secrets.SOPS_AGE_KEY }}
run : |
sops -d secrets.enc.yaml > secrets.yaml
- name : Deploy
run : ./deploy.sh
The only secret in GitHub is SOPS_AGE_KEY. Everything else lives in encrypted files in the repo.
Kubernetes Integration# Use with Helm:
1
2
3
4
5
6
7
# values.enc.yaml (encrypted)
database :
password : ENC[AES256_GCM,data:...]
# helm-wrapper.sh
#!/bin/bash
sops -d values.enc.yaml | helm upgrade myapp ./chart -f -
Or use the SOPS secrets operator:
1
2
3
4
5
6
7
8
9
apiVersion : isindir.github.com/v1alpha3
kind : SopsSecret
metadata :
name : app-secrets
spec :
secretTemplates :
- name : db-credentials
stringData :
password : ENC[AES256_GCM,data:...]
Partial Encryption# Only encrypt sensitive fields:
1
2
3
4
5
# .sops.yaml
creation_rules :
- path_regex : \.enc\.yaml$
encrypted_regex : "^(password|secret|key|token)$"
age : age1...
Now only matching keys get encrypted:
1
2
3
4
5
database :
host : db.example.com # plaintext
port : 5432 # plaintext
password : ENC[AES256_GCM...] # encrypted
username : app # plaintext
Rotation# When you need to re-encrypt with a new key:
1
2
3
4
5
# Update .sops.yaml with new key, then:
sops updatekeys secrets.enc.yaml
# Or rotate all files
find . -name "*.enc.yaml" -exec sops updatekeys {} \;
Git Diff for Encrypted Files# Add to .gitattributes:
. e n c . y a m l d i f f = s o p s d i f f e r
And .git/config:
1
2
[diff "sopsdiffer"]
textconv = sops -d
Now git diff shows decrypted content (if you have the key).
My Setup# 1
2
3
4
5
6
7
8
9
10
11
12
13
# .sops.yaml
creation_rules :
# Production: KMS with strict IAM
- path_regex : ^prod/.*\.enc\.(yaml|json)$
kms : arn:aws:kms:us-east-1:PROD_ACCOUNT:key/prod-key
# Staging: KMS with looser access
- path_regex : ^staging/.*\.enc\.(yaml|json)$
kms : arn:aws:kms:us-east-1:STAGING_ACCOUNT:key/staging-key
# Local dev: age key on my machine
- path_regex : \.enc\.(yaml|json)$
age : age1...
Secrets in git. Full history. Environment isolation. No plaintext anywhere.
That’s the dream.