SOPS
A secrets management tool that encrypts files while keeping keys and structure visible for version control and GitOps workflows.
SOPS Cheatsheet
A secrets management tool that encrypts files while keeping keys and structure visible for version control and GitOps workflows.
Overview
SOPS (Secrets OPerationS) is an editor of encrypted files that supports YAML, JSON, ENV, INI, and binary formats. It integrates with cloud KMS providers (AWS, GCP, Azure), PGP, and age for key management, making it ideal for storing secrets in Git repositories.
flowchart TB
subgraph Input["Input Files"]
YAML[YAML]
JSON[JSON]
ENV[ENV]
INI[INI]
BIN[Binary]
end
subgraph SOPS["SOPS Encryption"]
direction TB
Parse[Parse File] --> Identify[Identify Values]
Identify --> Encrypt[Encrypt Values]
Encrypt --> Metadata[Add Metadata]
end
subgraph Keys["Key Providers"]
KMS[AWS KMS]
GCP[GCP KMS]
Azure[Azure Key Vault]
PGP[PGP/GPG]
AGE[age]
end
subgraph Output["Encrypted Output"]
EncFile[Encrypted File<br/>Keys Visible<br/>Values Encrypted]
end
Input --> SOPS
Keys --> SOPS
SOPS --> Output
style SOPS fill:#e1f5fe
style Keys fill:#f3e5f5
style Output fill:#e8f5e9
File Encryption and Decryption
Key Concepts
- Value-level encryption: SOPS encrypts only the values, leaving keys and structure intact for easy diffing
- MAC (Message Authentication Code): Ensures file integrity and prevents tampering
- Metadata: Encrypted files contain a
sopssection with encryption details and key information - In-place encryption: Files can be encrypted/decrypted in place or to stdout
Common Commands
# Encrypt a file (requires .sops.yaml config or explicit key)
sops --encrypt secrets.yaml > secrets.enc.yaml
# Encrypt in place
sops --encrypt --in-place secrets.yaml
# Decrypt to stdout
sops --decrypt secrets.enc.yaml
# Decrypt in place
sops --decrypt --in-place secrets.enc.yaml
# Decrypt specific key to stdout
sops --decrypt --extract '["database"]["password"]' secrets.enc.yaml
# Encrypt with specific key type
sops --encrypt --age age1... secrets.yaml > secrets.enc.yaml
sops --encrypt --pgp FINGERPRINT secrets.yaml > secrets.enc.yaml
sops --encrypt --kms arn:aws:kms:... secrets.yaml > secrets.enc.yaml
# Rotate data key (re-encrypt with new data key)
sops --rotate --in-place secrets.enc.yaml
# Check encryption status (the sops metadata block is plaintext in the
# file itself, so just read it; there is no --show-metadata flag)
sops filestatus secrets.enc.yaml # {"encrypted":true}
grep -A20 '^sops:' secrets.enc.yaml
Examples
Basic encryption workflow:
# Create a plain secrets file
cat > secrets.yaml << EOF
database:
host: localhost
username: admin
password: supersecret123
api:
key: abc123def456
EOF
# Encrypt with age key
export SOPS_AGE_KEY_FILE=~/.sops/age-key.txt
sops --encrypt --age age1abc123... secrets.yaml > secrets.enc.yaml
# View encrypted file (keys visible, values encrypted)
cat secrets.enc.yaml
# database:
# host: ENC[AES256_GCM,data:...,tag:...]
# username: ENC[AES256_GCM,data:...,tag:...]
# password: ENC[AES256_GCM,data:...,tag:...]
# api:
# key: ENC[AES256_GCM,data:...,tag:...]
# sops:
# age:
# - recipient: age1abc123...
# enc: |
# -----BEGIN AGE ENCRYPTED FILE-----
# ...
Partial decryption:
# Extract only the database password
sops --decrypt --extract '["database"]["password"]' secrets.enc.yaml
# Output: supersecret123
# Extract entire database section as JSON
sops --decrypt --extract '["database"]' --output-type json secrets.enc.yaml
Key Management
Key Concepts
flowchart LR
subgraph Master["Master Keys"]
KMS[AWS KMS]
GCP[GCP KMS]
Azure[Azure KV]
PGP[PGP Key]
AGE[age Key]
end
subgraph Data["Data Key"]
DK[Symmetric Data Key<br/>Generated per file]
end
subgraph Encryption["Encryption"]
Values[Encrypt Values<br/>with Data Key]
end
Master -->|Encrypt Data Key| DK
DK -->|Encrypt| Values
style Master fill:#ffecb3
style DK fill:#c8e6c9
style Encryption fill:#bbdefb
- Master keys: External keys (KMS, PGP, age) that encrypt the data key
- Data key: Symmetric key generated per file, encrypts actual values
- Key groups: Require multiple keys from different groups for decryption
- Shamir's Secret Sharing: Split data key across multiple key groups
Common Commands
# Add/remove keys: the --add-*/--rm-* flags only take effect alongside
# --rotate (-r), which also re-encrypts with a fresh data key
sops --rotate -i --add-age age1newkey... secrets.enc.yaml
sops --rotate -i --add-pgp NEWFINGERPRINT secrets.enc.yaml
sops --rotate -i --add-kms arn:aws:kms:region:account:key/id secrets.enc.yaml
sops --rotate -i --rm-age age1oldkey... secrets.enc.yaml
sops --rotate -i --rm-pgp OLDFINGERPRINT secrets.enc.yaml
# Preferred: declare keys in .sops.yaml, then sync without rotating the
# data key (no --rotate needed)
sops updatekeys secrets.enc.yaml
# List keys in file (the sops metadata block is plaintext)
grep -A 100 "^sops:" secrets.enc.yaml
Examples
Multi-key configuration with key groups:
# .sops.yaml
creation_rules:
# Production secrets require keys from both groups
- path_regex: production/.*\.yaml$
key_groups:
- pgp:
- FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 # Security team
age:
- age1security...
- kms:
- arn:aws:kms:eu-west-1:123456789:key/prod-key
shamir_threshold: 2 # Need keys from 2 groups
# Development secrets - single key sufficient
- path_regex: development/.*\.yaml$
age: age1dev...
# Default for other files
- path_regex: .*\.yaml$
pgp: FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4
AWS KMS key policy for SOPS:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789:role/sops-role"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*"
}
]
}
Integration with GitOps Workflows
Key Concepts
- Git-friendly encryption: Keys remain visible for meaningful diffs
- Flux/ArgoCD integration: Native support for SOPS-encrypted secrets
- Kubernetes Secrets: Decrypt at deployment time or use operators
- Pre-commit hooks: Prevent committing unencrypted secrets
Common Patterns
# Flux Kustomization with SOPS decryption
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app-secrets
namespace: flux-system
spec:
interval: 10m
path: ./secrets
prune: true
sourceRef:
kind: GitRepository
name: app-repo
decryption:
provider: sops
secretRef:
name: sops-age # Contains age private key
# ArgoCD with SOPS using Helm secrets plugin
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
source:
repoURL: https://github.com/org/repo
path: charts/my-app
plugin:
name: helm-secrets
env:
- name: HELM_SECRETS_SOPS_PATH
value: /usr/bin/sops
Examples
Pre-commit hook to prevent plain secrets:
#!/bin/bash
# .git/hooks/pre-commit
# Check for unencrypted secrets files
for file in $(git diff --cached --name-only | grep -E 'secrets.*\.yaml$'); do
if ! grep -q "^sops:" "$file"; then
echo "ERROR: $file appears to be unencrypted!"
echo "Run: sops --encrypt --in-place $file"
exit 1
fi
done
Flux SOPS secret setup:
# Generate age key pair
age-keygen -o age-key.txt
# Create Kubernetes secret with private key
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=age-key.txt
# Add public key to .sops.yaml
cat >> .sops.yaml << EOF
creation_rules:
- path_regex: .*\.yaml$
encrypted_regex: ^(data|stringData)$
age: $(grep "public key:" age-key.txt | cut -d: -f2 | tr -d ' ')
EOF
Editing Encrypted Files
Key Concepts
- Direct editing: SOPS decrypts, opens editor, re-encrypts on save
- Editor selection: Uses $EDITOR, falls back to vim
- Atomic operations: Changes only saved if encryption succeeds
- Format preservation: Maintains YAML/JSON structure and comments
Common Commands
# Edit encrypted file (decrypts, opens editor, re-encrypts)
sops secrets.enc.yaml
# Specify editor
EDITOR=nano sops secrets.enc.yaml
# Edit with VS Code (requires waiting for close)
EDITOR="code --wait" sops secrets.enc.yaml
# Set specific values without opening editor
sops --set '["database"]["password"] "newpassword"' secrets.enc.yaml
# Set nested values
sops --set '["api"]["keys"][0] "newkey"' secrets.enc.yaml
Examples
Automated secret updates in scripts:
#!/bin/bash
# update-db-password.sh
NEW_PASSWORD=$(openssl rand -base64 32)
# Update password in encrypted file
sops --set "[\"database\"][\"password\"] \"$NEW_PASSWORD\"" secrets.enc.yaml
# Also update in password manager/vault
vault kv put secret/database password="$NEW_PASSWORD"
echo "Database password rotated successfully"
Comparing encrypted files:
# Decrypt and diff two files
diff <(sops -d secrets-old.enc.yaml) <(sops -d secrets-new.enc.yaml)
# Git diff driver for SOPS files
# Add to .gitattributes: *.enc.yaml diff=sops
git config diff.sops.textconv "sops -d"
age Support
Key Concepts
- Modern alternative to PGP: Simpler, smaller, and more secure
- No key servers: Keys are standalone files or strings
- Native X25519: Fast, secure elliptic curve cryptography
- Identity files: Private keys stored in simple text files
Common Commands
# Generate age key pair
age-keygen -o ~/.sops/age-key.txt
# Outputs public key: age1abc123...
# Set age key for SOPS
export SOPS_AGE_KEY_FILE=~/.sops/age-key.txt
# Or embed key directly (not recommended)
export SOPS_AGE_KEY="AGE-SECRET-KEY-1..."
# Encrypt with age
sops --encrypt --age age1abc123... secrets.yaml > secrets.enc.yaml
# Multiple age recipients
sops --encrypt \
--age age1user1...,age1user2...,age1ci... \
secrets.yaml > secrets.enc.yaml
Examples
Complete age setup for team:
# Each team member generates their key
age-keygen -o ~/.sops/key.txt
# Collect public keys in repository
mkdir -p .sops/keys
echo "age1alice..." > .sops/keys/alice.pub
echo "age1bob..." > .sops/keys/bob.pub
echo "age1ci..." > .sops/keys/ci.pub
# Create .sops.yaml with all recipients
cat > .sops.yaml << EOF
creation_rules:
- path_regex: .*\.enc\.yaml$
age: >-
age1alice...,
age1bob...,
age1ci...
EOF
# Encrypt new secrets (automatically uses all recipients)
sops --encrypt secrets.yaml > secrets.enc.yaml
Converting from PGP to age:
# Decrypt with old PGP key
sops --decrypt secrets.enc.yaml > secrets.yaml
# Re-encrypt with new age key
sops --encrypt --age age1newkey... secrets.yaml > secrets.enc.yaml
# Verify and clean up
sops --decrypt secrets.enc.yaml
rm secrets.yaml
Best Practices for Secrets Management
Key Concepts
- Least privilege: Only necessary systems should have decryption access
- Key rotation: Regularly rotate both master and data keys
- Audit trail: Log all decryption operations
- Separation of concerns: Different keys for different environments
Configuration Patterns
# .sops.yaml - Environment separation
creation_rules:
# Production - strict access
- path_regex: ^production/.*$
kms: arn:aws:kms:eu-west-1:123456789:key/prod-key
gcp_kms: projects/prod/locations/global/keyRings/sops/cryptoKeys/prod
# Staging - moderate access
- path_regex: ^staging/.*$
kms: arn:aws:kms:eu-west-1:123456789:key/staging-key
# Development - team access
- path_regex: ^development/.*$
age: age1devteam...
# Encrypted regex for Kubernetes secrets
- path_regex: .*secret.*\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1k8s...
Examples
Structured secrets repository:
secrets/
├── .sops.yaml # SOPS configuration
├── .gitattributes # Git diff driver
├── production/
│ ├── database.enc.yaml
│ ├── api-keys.enc.yaml
│ └── certificates.enc.yaml
├── staging/
│ ├── database.enc.yaml
│ └── api-keys.enc.yaml
└── development/
└── local.enc.yaml
Key rotation script:
#!/bin/bash
# rotate-keys.sh
# Find all encrypted files
find . -name "*.enc.yaml" | while read -r file; do
echo "Rotating keys for: $file"
# Update keys based on .sops.yaml
sops updatekeys --yes "$file"
# Rotate data key
sops --rotate --in-place "$file"
done
echo "Key rotation complete"
Encrypted regex for partial encryption:
# .sops.yaml - Only encrypt specific fields
creation_rules:
- path_regex: .*\.yaml$
encrypted_regex: ^(password|secret|key|token|credential)$
age: age1...
Integration with CI/CD Pipelines
Key Concepts
flowchart LR
subgraph CI["CI/CD Pipeline"]
Checkout[Checkout Code] --> Decrypt[Decrypt Secrets]
Decrypt --> Build[Build/Deploy]
Build --> Cleanup[Cleanup Secrets]
end
subgraph Keys["Key Access"]
KMS[Cloud KMS<br/>via IAM Role]
AGE[age Key<br/>via CI Secret]
end
Keys -->|Authenticate| Decrypt
style CI fill:#e3f2fd
style Keys fill:#fff3e0
- Ephemeral decryption: Decrypt only when needed, clean up immediately
- IAM roles: Use cloud IAM for KMS access instead of static credentials
- Masked outputs: Ensure decrypted values don't appear in logs
- Limited scope: Decrypt only required files
Examples
GitHub Actions workflow:
name: Deploy with SOPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # For AWS OIDC
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: eu-west-1
- name: Install SOPS
run: |
curl -LO https://github.com/getsops/sops/releases/download/v3.11.0/sops-v3.11.0.linux.amd64
chmod +x sops-v3.11.0.linux.amd64
sudo mv sops-v3.11.0.linux.amd64 /usr/local/bin/sops
- name: Decrypt secrets
run: |
sops --decrypt config/secrets.enc.yaml > secrets.yaml
- name: Deploy application
run: |
./deploy.sh
- name: Cleanup
if: always()
run: rm -f secrets.yaml
GitLab CI with age:
# .gitlab-ci.yml
variables:
SOPS_AGE_KEY_FILE: /tmp/age-key.txt
deploy:
stage: deploy
before_script:
# Write age key from CI variable
- echo "$SOPS_AGE_KEY" > $SOPS_AGE_KEY_FILE
- chmod 600 $SOPS_AGE_KEY_FILE
script:
- sops --decrypt secrets.enc.yaml > secrets.yaml
- kubectl apply -f secrets.yaml
after_script:
# Always clean up secrets
- rm -f secrets.yaml $SOPS_AGE_KEY_FILE
Jenkins pipeline:
pipeline {
agent any
environment {
SOPS_AGE_KEY = credentials('sops-age-key')
}
stages {
stage('Decrypt Secrets') {
steps {
script {
// Write key to file
writeFile file: '/tmp/age-key.txt', text: SOPS_AGE_KEY
// Decrypt secrets
sh '''
export SOPS_AGE_KEY_FILE=/tmp/age-key.txt
sops --decrypt secrets.enc.yaml > secrets.yaml
'''
}
}
}
stage('Deploy') {
steps {
sh './deploy.sh'
}
}
}
post {
always {
sh 'rm -f secrets.yaml /tmp/age-key.txt'
}
}
}
Quick Reference
| Operation | Command |
|---|---|
| Encrypt file | sops --encrypt secrets.yaml > secrets.enc.yaml |
| Encrypt in place | sops --encrypt --in-place secrets.yaml |
| Decrypt to stdout | sops --decrypt secrets.enc.yaml |
| Decrypt in place | sops --decrypt --in-place secrets.enc.yaml |
| Edit encrypted file | sops secrets.enc.yaml |
| Set value | sops --set '["key"] "value"' file.enc.yaml |
| Extract value | sops -d --extract '["key"]' file.enc.yaml |
| Rotate data key | sops --rotate --in-place file.enc.yaml |
| Update keys from config | sops updatekeys file.enc.yaml |
| Add age key | sops -r -i --add-age age1... file.enc.yaml |
| Remove age key | sops -r -i --rm-age age1... file.enc.yaml |
| Check encryption status | sops filestatus file.enc.yaml |
| Generate age key | age-keygen -o key.txt |
| Encrypt with age | sops --encrypt --age age1... file.yaml |
| Encrypt with KMS | sops --encrypt --kms arn:aws:kms:... file.yaml |
| Encrypt with PGP | sops --encrypt --pgp FINGERPRINT file.yaml |
Common Issues and Solutions
Issue: "could not decrypt data key"
Cause: Missing or incorrect decryption key
Solution:
# Check which keys are required (metadata block is plaintext in the file)
grep -A50 '^sops:' secrets.enc.yaml
# Verify age key is set
echo $SOPS_AGE_KEY_FILE
cat $SOPS_AGE_KEY_FILE
# Verify KMS access
aws kms describe-key --key-id arn:aws:kms:...
# Verify PGP key is imported
gpg --list-secret-keys FINGERPRINT
Issue: "MAC mismatch" error
Cause: File was modified without re-encrypting or corrupted
Solution:
# If you have the original data, re-encrypt
sops --encrypt original.yaml > new.enc.yaml
# Or decrypt ignoring MAC (use with caution)
sops --decrypt --ignore-mac corrupted.enc.yaml
Issue: Files not being encrypted with correct rules
Cause: .sops.yaml regex patterns not matching
Solution:
# Test which rule matches
sops --verbose --encrypt test-file.yaml
# Check .sops.yaml syntax and regex patterns
# Ensure path_regex matches from repository root
# Use anchors: ^path/.*\.yaml$
Issue: "age: no identity matched any of the recipients"
Cause: Public key in encrypted file doesn't match private key
Solution:
# List recipients in encrypted file (metadata block is plaintext)
grep recipient file.enc.yaml
# Check your public key
grep "public key:" ~/.sops/key.txt
# Add your key to the file (requires access to another valid key;
# --add-age only applies with --rotate)
sops --rotate -i --add-age $(grep "public key:" ~/.sops/key.txt | cut -d: -f2 | tr -d ' ') file.enc.yaml
Issue: Kubernetes secrets show encrypted values
Cause: Not using encrypted_regex for Kubernetes secrets
Solution:
# .sops.yaml
creation_rules:
- path_regex: .*secret.*\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1...
Issue: Large files are slow to encrypt/decrypt
Cause: SOPS processes each value individually
Solution:
# Split into smaller files by service/component
# Or use unencrypted_regex to skip non-sensitive data
# .sops.yaml
creation_rules:
- path_regex: .*\.yaml$
unencrypted_regex: ^(metadata|kind|apiVersion)$
age: age1...
Issue: AWS KMS "AccessDeniedException"
Cause: IAM permissions insufficient for KMS operations
Solution:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:region:account:key/key-id"
}
]
}
Related Topics
The following topics would complement this SOPS cheatsheet:
- HashiCorp Vault - Dynamic secrets management, secrets injection, and enterprise-grade secrets infrastructure
- Sealed Secrets (Bitnami) - Kubernetes-native encrypted secrets that can only be decrypted by the cluster controller
- External Secrets Operator - Kubernetes operator that synchronises secrets from external providers (Vault, AWS Secrets Manager)
- age encryption - Deep dive into age key management, identity files, and advanced encryption patterns
- Flux CD - GitOps toolkit with native SOPS integration for Kubernetes deployments
- AWS KMS / GCP Cloud KMS - Cloud key management services for enterprise key rotation, access policies, and audit logging