TL;DR — Quick Summary
SOPS encrypts only values in YAML/JSON, keeping keys readable for diffing. Full guide: age, KMS, key rotation, CI/CD, Kubernetes, and ArgoCD GitOps.
Mozilla SOPS (Secrets OPerationS) solves one of GitOps’ hardest problems: how to store secrets in version control without exposing them. Unlike tools that encrypt entire files, SOPS encrypts only the values in YAML, JSON, ENV, and INI files — keeping keys plaintext so diffs remain readable and code reviews stay meaningful.
Why SOPS? The Secrets-in-Git Problem
Committing plaintext secrets to Git is a security incident waiting to happen. The alternatives each have tradeoffs:
- Environment variables only — Works for runtime but doesn’t version or audit secret changes.
- Vault / External Secrets — Powerful but requires infrastructure and online access at deploy time.
- git-crypt — Encrypts whole files; diffs become binary blobs and code review is impossible.
- SOPS — Encrypts only values. Keys stay plaintext. Files diff normally. Works offline with age.
A SOPS-encrypted file looks like this:
database:
password: ENC[AES256_GCM,data:abc123...,iv:...,tag:...,type:str]
host: db.example.com # ← unencrypted, not a secret
The structure is auditable. The secret is protected. Git history tracks which values changed (by seeing the encrypted blob change) without revealing what changed.
Installation
# macOS
brew install sops age
# Ubuntu/Debian
apt install age
wget https://github.com/getsops/sops/releases/latest/download/sops-v3.x.x.linux.amd64 \
-O /usr/local/bin/sops && chmod +x /usr/local/bin/sops
# Docker (for CI)
docker run --rm -v $(pwd):/work ghcr.io/getsops/sops:latest
Encryption Backends
| Backend | Best For | Infrastructure Needed |
|---|---|---|
| age | Simple, modern default | None |
| PGP/GPG | Legacy, widely supported | GPG keyring |
| AWS KMS | AWS-native teams | AWS account + IAM |
| GCP KMS | GCP-native teams | GCP project |
| Azure Key Vault | Azure/Microsoft shops | Azure subscription |
| HashiCorp Vault | Multi-cloud, on-prem | Vault cluster |
For new projects, age is the recommended default. It has no dependencies, generates keys in seconds, and supports multiple recipients without a keyserver.
age Backend Deep Dive
age is a modern encryption tool designed to replace GPG for file encryption. It is simpler, has no concept of a “web of trust,” and the key format is human-friendly.
# Generate a key pair
age-keygen -o ~/.config/sops/age/keys.txt
# Public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Set env var so SOPS finds your private key
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
# Multiple recipients (team members all get access)
# In .sops.yaml:
# recipients: age1alice...,age1bob...,age1charlie...
age also supports SSH keys as recipients (age1ssh-ed25519...), which is useful if your team already has SSH keys but has not generated dedicated age keys.
.sops.yaml Configuration
.sops.yaml at the root of your repo controls which keys encrypt which files:
creation_rules:
- path_regex: secrets/.*\.yaml$
age: >-
age1alice1111111111111111111111111111111111111111111111111111111,
age1bob2222222222222222222222222222222222222222222222222222222222
- path_regex: k8s/.*secret.*\.yaml$
kms: "arn:aws:kms:us-east-1:123456789:key/mrk-abc123"
age: age1alice1111111111111111111111111111111111111111111111111111111
Key groups and Shamir’s Secret Sharing let you require M of N parties to decrypt:
creation_rules:
- path_regex: critical/.*
key_groups:
- age:
- age1alice...
- age1bob...
- kms:
- arn: "arn:aws:kms:us-east-1:123456789:key/mrk-abc123"
shamir_threshold: 2 # requires 2 of the above key groups
This is useful for disaster recovery: even if your KMS is unreachable, two team members can decrypt together using their age keys.
Encrypting and Editing Files
# Encrypt in place (requires SOPS_AGE_KEY_FILE set)
sops --encrypt secrets.yaml > secrets.enc.yaml
# Or encrypt and overwrite
sops --encrypt --in-place secrets.yaml
# Open in editor — SOPS decrypts, you edit, it re-encrypts on save
sops secrets.enc.yaml
# Decrypt to stdout
sops --decrypt secrets.enc.yaml
# Inject as env vars into a command
sops exec-env secrets.enc.yaml 'docker-compose up'
# Supported formats: YAML, JSON, ENV, INI, binary
sops --encrypt --input-type dotenv .env > .env.enc
AWS KMS Integration
# Create a KMS key (or use existing)
aws kms create-key --description "SOPS encryption key"
# Encrypt using KMS ARN
sops --kms "arn:aws:kms:us-east-1:ACCOUNT:key/KEY-ID" --encrypt secrets.yaml
# IAM policy for SOPS (attach to CI role / developer role)
# kms:Encrypt, kms:Decrypt, kms:GenerateDataKey, kms:DescribeKey
For cross-account access, add the target account’s role ARN to the KMS key policy and use aws_profile in .sops.yaml.
KMS key rotation is transparent to SOPS: AWS auto-rotates the data key on your schedule without changing the ARN or requiring SOPS file re-encryption.
Key Rotation
Rotate keys when a team member leaves or a key is compromised:
# 1. Add new key, remove old key from .sops.yaml
# 2. Re-encrypt all affected files
sops updatekeys --yes secrets/db.yaml
sops updatekeys --yes secrets/api.yaml
# 3. Verify old key no longer works
SOPS_AGE_KEY="old_private_key" sops --decrypt secrets/db.yaml
# Should fail: "could not decrypt data key"
# 4. Revoke old key from any team keyring or KMS policy
CI/CD Integration
GitHub Actions with age:
- name: Decrypt secrets
env:
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
run: |
sops --decrypt k8s/secrets.enc.yaml > k8s/secrets.yaml
kubectl apply -f k8s/secrets.yaml
GitLab CI:
decrypt_secrets:
variables:
SOPS_AGE_KEY: $SOPS_AGE_KEY_SECRET
script:
- sops --decrypt secrets.enc.yaml | kubectl apply -f -
ArgoCD with KSOPS plugin:
KSOPS is a kustomize plugin that transparently decrypts SOPS-encrypted Secret manifests before ArgoCD applies them. Install the plugin in the ArgoCD repo-server and reference it in your kustomization.yaml:
# kustomization.yaml
generators:
- secret-generator.yaml # KSOPS generator pointing to encrypted files
Flux with SOPS controller:
Flux has native SOPS support. Create a Secret containing your age private key and configure the Kustomization resource with spec.decryption.provider: sops.
Kubernetes Secrets Workflow
# 1. Create a plaintext Secret manifest
kubectl create secret generic db-creds \
--from-literal=password=supersecret \
--dry-run=client -o yaml > k8s/db-secret.yaml
# 2. Encrypt it with SOPS
sops --encrypt --in-place k8s/db-secret.yaml
# 3. Commit the encrypted file — safe to store in Git
git add k8s/db-secret.yaml && git commit -m "Add encrypted db secret"
# 4. Decrypt and apply (or let ArgoCD/Flux do it)
sops --decrypt k8s/db-secret.yaml | kubectl apply -f -
Git Diff Integration
SOPS provides a diff driver so git diff shows decrypted content locally:
# .gitattributes
*.enc.yaml diff=sops
# .gitconfig (local)
[diff "sops"]
textconv = sops --decrypt
Now git diff on encrypted files shows the actual value changes (for team members with the key), while the committed file remains encrypted.
SOPS vs Alternatives
| Tool | Encrypts | Diffable | Infra Needed | GitOps Friendly |
|---|---|---|---|---|
| SOPS | Values only | Yes | Optional (age) | Excellent |
| Sealed Secrets | Whole k8s Secret | No | Cluster controller | Kubernetes only |
| External Secrets | Values at runtime | N/A | Vault/KMS + controller | Yes, online only |
| git-crypt | Whole files | No | GPG keyring | Limited |
| BlackBox | Whole files | No | PGP keyring | Limited |
| Vault Agent | Values at runtime | N/A | Vault cluster | Yes, online only |
SOPS is the only tool that keeps files diffable, supports offline decryption, and integrates natively with both ArgoCD (via KSOPS) and Flux.
Gotchas and Edge Cases
- Never commit the private key — Only commit the public key in
.sops.yaml. The private key goes in CI secrets or a password manager. .sops.yamlmust be at repo root — Or pass--config path/to/.sops.yamlexplicitly.- Editor mode re-encrypts all values — Running
sops file.yamlrotates the data key. This is intentional but makes diffs noisy. Use--encrypt/--decryptfor automation. - Binary format for non-text files — Use
--input-type binary --output-type binaryfor files that are not YAML/JSON/ENV. - Shamir threshold must be met at decrypt time — If KMS is unavailable and you set
shamir_threshold: 2, you need 2 age key holders present simultaneously. - age key file permissions — SOPS will refuse to use a keys.txt file readable by other users. Run
chmod 600 ~/.config/sops/age/keys.txt.
Summary
- SOPS encrypts values, not keys — Files stay diff-friendly and auditable in Git.
- age is the modern default — No infrastructure, works offline, supports multiple recipients.
.sops.yamlcreation_rules — Map path patterns to encryption keys with key groups and Shamir threshold for M-of-N access control.- Native Kubernetes support — KSOPS for ArgoCD, native provider for Flux.
- Key rotation —
sops updatekeysre-encrypts files without changing their structure.