Why SOPS + Age?
Managing secrets in version control has always been a dilemma. You want infrastructure as code, but you can't commit plaintext credentials. Traditional solutions force you into complex trade-offs:
- HashiCorp Vault: Powerful but requires running infrastructure, HA setup, and operational expertise
- AWS Secrets Manager/Azure Key Vault: Cloud-vendor locked, additional costs, doesn't solve the "secrets in Git" problem
- git-crypt/transcrypt: Encrypts entire files, poor diff visibility, hard to rotate keys
- .env files in private repos: No granular access control, no audit trail, still visible to anyone with repo access
Mozilla SOPS takes a different approach: encrypt values, not files. It integrates with your existing workflows—Git, diff tools, CI/CD—while keeping secrets encrypted at rest. Paired with Age, the modern encryption tool by Filippo Valsorda, you get a secrets management solution that's simple, secure, and maintainable by small teams.
Traditional Tools
- Requires running infrastructure
- Operational complexity
- Network dependency
- Vendor lock-in risks
- Higher costs
SOPS + Age
- No infrastructure to run
- Works offline
- Git-native workflow
- Vendor independent
- Free and open source
How SOPS Works
SOPS uses a technique called envelope encryption. Here's the elegant simplicity:
- Data Key Generation: When encrypting, SOPS generates a random 256-bit data key
- Content Encryption: Your actual secrets are encrypted with this data key using AES-256-GCM
- Key Encryption: The data key itself is encrypted with your Age (or other) public keys
- Metadata Storage: Encrypted data keys are stored in the file's metadata
This means multiple people can decrypt the same file using their individual private keys. Rotate a team member out? Just remove their encrypted data key from the metadata—no re-encryption of actual secrets needed.
SOPS only encrypts values, not keys. Your YAML/JSON structure remains visible, making diffs meaningful and code review possible. You can see that database.password changed, just not what it changed to.
Installation
Install Age
# macOS
brew install age
# Linux (Debian/Ubuntu)
sudo apt-get install age
# Linux (Arch)
sudo pacman -S age
# Or download binary
curl -LO https://github.com/FiloSottile/age/releases/latest/download/age-linux-amd64.tar.gz
tar xf age-linux-amd64.tar.gz
sudo mv age/age /usr/local/bin/
sudo mv age/age-keygen /usr/local/bin/
Install SOPS
# macOS
brew install sops
# Linux
curl -LO https://github.com/getsops/sops/releases/latest/download/sops-linux-amd64
chmod +x sops-linux-amd64
sudo mv sops-linux-amd64 /usr/local/bin/sops
# Verify
sops --version
Generate Your Age Key
# Generate a new key pair
age-keygen -o ~/.config/sops/age/keys.txt
# View your public key (used for encryption)
age-keygen -y ~/.config/sops/age/keys.txt
# Output: age1ql3z7... (your public key)
Basic Usage
Configure SOPS for Age
Create a SOPS configuration file in your repo root:
# Define creation rules - who can decrypt new files
creation_rules:
- age: |
# Add all team member public keys here
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3pj2etdfg... # Alice
age1g47h9dy... # Bob
age1f8h2k... # CI/CD key
# Path-specific rules (optional)
- path_regex: production/.*
age: |
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3pj2etdfg... # Senior team only
Encrypt Your First File
Create a Plaintext Secret
# secrets.yaml
database:
host: prod-db.internal
port: 5432
username: app_user
password: SuperSecret123!
api:
key: sk_live_abcdef123456
endpoint: https://api.example.com
Encrypt It
sops encrypt secrets.yaml > secrets.enc.yaml
# Or edit in place with automatic encryption
sops secrets.yaml # Opens in $EDITOR, encrypts on save
View Encrypted Output
# Result: secrets.enc.yaml
database:
host: prod-db.internal
port: 5432
username: app_user
password: ENC[AES256_GCM,data:abc...,iv:def...,tag:ghi...,type:str]
api:
key: ENC[AES256_GCM,data:jkl...,iv:mno...,tag:pqr...,type:str]
endpoint: https://api.example.com
sops:
age:
- recipient: age1ql3z7hjy54pw3...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-03-18T08:00:00Z"
version: 3.9.0
Decrypt and Use
# Decrypt to stdout
sops decrypt secrets.enc.yaml
# Decrypt to file
sops decrypt secrets.enc.yaml > secrets.yaml
# Extract specific value
sops decrypt --extract '["database"]["password"]' secrets.enc.yaml
Working with Different Formats
SOPS supports multiple file formats with intelligent value detection:
| Format | Extension | Encrypted Fields |
|---|---|---|
| YAML | .yaml, .yml | All values by default, keys visible |
| JSON | .json | All values by default |
| ENV | .env | Values only, variable names visible |
| INI | .ini | Values only |
| Binary | any | Entire file encrypted |
Partial Encryption
Not everything needs encryption. Configure SOPS to only encrypt specific paths:
creation_rules:
- path_regex: config/.*\.yaml
encrypted_regex: '^(password|secret|key|token)$'
age: |
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3pj2etdfg...
- path_regex: production/.*\.env
age: |
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3pj2etdfg...
Encrypted Comments
SOPS preserves and encrypts comments too:
database:
# This is the production password - DO NOT SHARE
password: SecretPassword123
database:
#ENC[AES256_GCM,data:abc...,iv:def...,type:comment]
password: ENC[AES256_GCM,data:ghi...,iv:jkl...,type:str]
Key Groups & Multiple Keys
SOPS supports sophisticated key management scenarios:
Shamir Secret Sharing
Require multiple keys to decrypt (M-of-N threshold):
creation_rules:
- shamir_threshold: 2
key_groups:
- age:
- age1ql3z7hjy54pw3... # Alice
- age:
- age1g47h9dy... # Bob
- age:
- age1f8h2k... # Charlie
# Any 2 of 3 can decrypt
Multiple KMS Providers
Mix Age with cloud KMS for recovery scenarios:
creation_rules:
- key_groups:
- age:
- age1ql3z7hjy54pw3... # Team key
- kms:
- arn: arn:aws:kms:us-east-1:123456789:key/abc123
role: arn:aws:iam::123456789:role/sops-role
Git Integration
Meaningful Diffs
SOPS integrates with Git for encrypted diffs:
# Add to .gitattributes
*.yaml diff=sopsdiffer
*.yml diff=sopsdiffer
*.json diff=sopsdiffer
*.env diff=sopsdiffer
# Configure git diff driver
git config diff.sopsdiffer.textconv "sops decrypt"
Pre-commit Hooks
Prevent committing plaintext secrets:
repos:
- repo: https://github.com/yuvipanda/pre-commit-hook-ensure-sops
rev: v1.0
hooks:
- id: sops-encryption
# Ensure all files matching these patterns are encrypted
files: ^(secrets|config|production)/.*\.(yaml|yml|json|env)$
exclude: ".sops.yaml|.*\.dec\.*"
Even with pre-commit hooks, always review your diffs before committing. Once a plaintext secret hits Git history, it's there forever—consider it compromised.
CI/CD Integration
GitHub Actions
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install SOPS and Age
run: |
wget -qO /usr/local/bin/sops \
"https://github.com/getsops/sops/releases/latest/download/sops-linux-amd64"
chmod +x /usr/local/bin/sops
sudo apt-get install age
- name: Setup Age key
env:
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
run: |
mkdir -p ~/.config/sops/age
echo "$AGE_SECRET_KEY" > ~/.config/sops/age/keys.txt
- name: Decrypt secrets
run: |
sops decrypt secrets.enc.yaml > secrets.yaml
export DB_PASSWORD=$(sops decrypt --extract '["database"]["password"]' secrets.enc.yaml)
- name: Deploy
run: |
# Your deployment commands
kubectl apply -f k8s/
GitLab CI
stages:
- deploy
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache age sops
- mkdir -p ~/.config/sops/age
- echo "$AGE_SECRET_KEY" > ~/.config/sops/age/keys.txt
script:
- export DB_PASSWORD=$(sops decrypt --extract '["database"]["password"]' secrets.enc.yaml)
- deploy-script.sh
Flux/GitOps
Flux has native SOPS support:
# Add Age public key to cluster
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=keys.txt
# Kustomization with SOPS decryption
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app
namespace: flux-system
spec:
interval: 10m
path: ./k8s
sourceRef:
kind: GitRepository
name: app
decryption:
provider: sops
Cloud KMS Integration
Age is the recommended approach, but SOPS supports cloud KMS for hybrid scenarios:
AWS KMS
# Create KMS key
aws kms create-key --description "SOPS key"
# Add to .sops.yaml
creation_rules:
- kms: arn:aws:kms:us-east-1:123456789:key/abc123
Azure Key Vault
creation_rules:
- azure_keyvault: https://myvault.vault.azure.net/keys/sops-key
GCP KMS
creation_rules:
- gcp_kms: projects/my-project/locations/global/keyRings/sops/cryptoKeys/my-key
Best Practices
Key Management
- Generate separate CI/CD keys: Don't use personal keys for automation
- Rotate keys quarterly: Re-encrypt files with new keys periodically
- Store Age private keys securely: Password managers, HSMs, or cloud secret stores—not in repos
- Use key groups for teams: Each team member gets their own key
File Organization
.
├── .sops.yaml # Root config
├── secrets/
│ ├── production/
│ │ ├── database.enc.yaml
│ │ └── api-keys.enc.yaml
│ └── staging/
│ ├── database.enc.yaml
│ └── api-keys.enc.yaml
├── k8s/
│ └── secrets/ # Kubernetes Secrets, encrypted
│ └── app-secrets.enc.yaml
└── terraform/
└── terraform.enc.tfvars
Rotation Strategies
- Update the actual secret value in the target system
- Encrypt new value with SOPS
- Commit and deploy
- Verify application uses new secret
- Revoke old secret in target system
Auditing
Git provides your audit trail:
# Who changed secrets last?
git log --oneline -10 -- secrets/production/
# What changed?
git log -p -- secrets/production/database.enc.yaml
# Blame (with SOPS diff)
git blame -L 1,50 secrets/production/database.enc.yaml
Conclusion
SOPS with Age provides a pragmatic middle ground for secrets management. You don't need to run Vault infrastructure, yet you get encryption, access control, and Git-native workflows. It's particularly well-suited for:
- Small to medium teams without dedicated security operations
- GitOps workflows where everything should be in Git
- Organizations with compliance requirements for encryption at rest
- Multi-cloud environments avoiding vendor lock-in
The simplicity is the point. When secrets management is easy, teams actually use it. When it's complex, they find workarounds. SOPS + Age removes the friction while maintaining security best practices.
Start with Age for simple setups, add cloud KMS when you need enterprise features, and evolve your approach as requirements grow. The foundation you build with SOPS will scale with your organization.