SOPS (Secrets Operations) Cheatsheet
Quick reference for SOPS — encrypt secrets in Git using AWS KMS, GCP KMS, age, or PGP. Covers basic operations, CI/CD integration, Helm, Terraform, and key rotation.
Quick Reference
Basic Operations
# Install SOPS
brew install sops
# or download binary from https://github.com/getsops/sops/releases
# Encrypt a file
sops --encrypt secrets.yaml > secrets.enc.yaml
sops -e secrets.yaml > secrets.enc.yaml
# Decrypt a file
sops --decrypt secrets.enc.yaml
sops -d secrets.enc.yaml
# Encrypt in-place (overwrites original)
sops --encrypt --in-place secrets.yaml
sops -e -i secrets.yaml
# Decrypt in-place
sops --decrypt --in-place secrets.enc.yaml
sops -d -i secrets.enc.yaml
# Edit encrypted file (decrypts, opens $EDITOR, re-encrypts)
sops secrets.enc.yaml
EDITOR=vim sops secrets.enc.yaml
# Re-encrypt with current .sops.yaml keys
sops updatekeys secrets.enc.yaml
AWS KMS
# Encrypt with AWS KMS key
sops --kms arn:aws:kms:us-east-1:123456789012:key/mrk-abc123 \
--encrypt secrets.yaml > secrets.enc.yaml
# Use AWS profile / region
export AWS_PROFILE=production
export AWS_REGION=us-east-1
sops -e secrets.yaml > secrets.enc.yaml
# Decrypt (uses credentials from environment)
sops -d secrets.enc.yaml
# Use with assume-role
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/sops-role \
--role-session-name sops-session \
| jq -r '.Credentials | "export AWS_ACCESS_KEY_ID=\(.AccessKeyId)\nexport AWS_SECRET_ACCESS_KEY=\(.SecretAccessKey)\nexport AWS_SESSION_TOKEN=\(.SessionToken)"' \
| source /dev/stdin
sops -d secrets.enc.yaml
# Create KMS key for SOPS
aws kms create-key --description "SOPS encryption key"
aws kms create-alias \
--alias-name alias/sops-key \
--target-key-id KEY_ID
GCP KMS
# Encrypt with GCP KMS
sops --gcp-kms \
projects/my-project/locations/global/keyRings/sops-keyring/cryptoKeys/sops-key \
--encrypt secrets.yaml > secrets.enc.yaml
# Set credentials
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa-key.json
# or use ADC (Application Default Credentials)
gcloud auth application-default login
# Create KMS resources for SOPS
gcloud kms keyrings create sops-keyring --location=global
gcloud kms keys create sops-key \
--location=global \
--keyring=sops-keyring \
--purpose=encryption
# Grant access to a service account
gcloud kms keys add-iam-policy-binding sops-key \
--location=global \
--keyring=sops-keyring \
--member="serviceAccount:[email protected]" \
--role="roles/cloudkms.cryptoKeyEncrypterDecrypter"
age Encryption
# Install age
brew install age
# Generate a key pair
age-keygen -o ~/.config/sops/age/keys.txt
# Output shows public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Set age keys file location
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
# Encrypt with age recipient (public key)
sops --age age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
--encrypt secrets.yaml > secrets.enc.yaml
# Decrypt (reads private key from SOPS_AGE_KEY_FILE)
sops --decrypt secrets.enc.yaml
# Multiple age recipients (any can decrypt)
sops --age age1abc...,age1def... \
--encrypt secrets.yaml > secrets.enc.yaml
# age key file format (~/.config/sops/age/keys.txt)
# # created: 2026-01-01T00:00:00+07:00
# # public key: age1ql3z7hjy...
# AGE-SECRET-KEY-1QJQKUQZ...
PGP (Legacy)
# Generate GPG key (for legacy systems)
gpg --full-generate-key
# List keys
gpg --list-keys
gpg --list-secret-keys
# Get key fingerprint
gpg --fingerprint [email protected]
# Encrypt with PGP fingerprint
sops --pgp FINGERPRINT1,FINGERPRINT2 \
--encrypt secrets.yaml > secrets.enc.yaml
# Import a collaborator's public key
gpg --import colleague.pub.gpg
gpg --trust-model always --import colleague.pub.gpg
# Export your public key (share with team)
gpg --export --armor [email protected] > mykey.pub.gpg
# Trust a key (required before encryption with it)
gpg --edit-key FINGERPRINT
gpg> trust
gpg> 5 # Ultimate trust
gpg> quit
Partial Encryption
# Encrypt only fields ending with _secret or _password
sops --encrypted-suffix "_secret" \
--encrypt config.yaml > config.enc.yaml
# Encrypt only fields NOT ending with _unencrypted
sops --unencrypted-suffix "_unencrypted" \
--encrypt config.yaml > config.enc.yaml
# Encrypt only matching fields (regex)
sops --encrypted-regex "^(password|secret|token|key)$" \
--encrypt config.yaml > config.enc.yaml
# Example input YAML:
# database:
# host: db.example.com # NOT encrypted (string)
# password: supersecret # Encrypted (matches regex)
# api_key: abc123 # Encrypted (matches regex)
# log_level: info # NOT encrypted
# View which fields are encrypted (keys are visible)
sops -d config.enc.yaml | head -20
.sops.yaml Configuration File
Tip: Place
.sops.yaml at the repository root. SOPS auto-discovers it by searching parent directories from the file being encrypted. This means you don't need to pass --kms flags manually.
# .sops.yaml — repository root
creation_rules:
# Production secrets: require AWS KMS (multi-region for HA) + age fallback
- path_regex: environments/production/.*\.yaml$
kms:
- arn: arn:aws:kms:us-east-1:123456789012:key/mrk-prod-key-1
aws_profile: production
role: arn:aws:iam::123456789012:role/sops-prod-role
- arn: arn:aws:kms:eu-west-1:123456789012:key/mrk-prod-key-2
aws_profile: production
role: arn:aws:iam::123456789012:role/sops-prod-role
gcp_kms: projects/my-project/locations/global/keyRings/prod/cryptoKeys/sops
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Staging secrets: single KMS key is fine
- path_regex: environments/staging/.*\.yaml$
kms:
- arn: arn:aws:kms:us-east-1:123456789012:key/mrk-staging-key
aws_profile: staging
age: age1staging...
# Developer secrets: age only (no cloud credentials needed locally)
- path_regex: environments/dev/.*\.yaml$
age: >-
age1dev1...,
age1dev2...
# Shamir Secret Sharing: require 2 of 3 keys to decrypt
- path_regex: secrets/critical/.*\.yaml$
key_groups:
- kms:
- arn: arn:aws:kms:us-east-1:123456789012:key/key-a
age:
- age1key-a...
- kms:
- arn: arn:aws:kms:us-east-1:123456789012:key/key-b
age:
- age1key-b...
- pgp:
- FINGERPRINT_C
shamir_threshold: 2 # Require 2 of 3 groups
Helm + SOPS (helm-secrets plugin)
# Install helm-secrets plugin
helm plugin install https://github.com/jkroepke/helm-secrets
# Verify installation
helm secrets version
# Encrypt a values file
helm secrets encrypt values/production/secrets.yaml
# Decrypt for inspection (outputs to stdout)
helm secrets decrypt values/production/secrets.yaml
# Install/upgrade using encrypted values
helm secrets upgrade --install myapp ./chart \
-f values/base.yaml \
-f secrets://values/production/secrets.yaml \
--namespace production
# List secrets in a directory
helm secrets dec values/production/
# Edit encrypted values
helm secrets edit values/production/secrets.yaml
# Helmfile integration with helm-secrets
# helmfile.yaml:
releases:
- name: myapp
chart: ./charts/myapp
values:
- values/base.yaml
- values/{{ .Environment.Name }}/values.yaml
secrets:
- values/{{ .Environment.Name }}/secrets.yaml # Auto-decrypted
# Run helmfile with secrets
helmfile -e production sync
helmfile -e production diff
Ansible + SOPS
# Install community.sops Ansible collection
ansible-galaxy collection install community.sops
# ansible.cfg
[defaults]
collections_paths = ./collections
# Use sops-encrypted vars file in a play
# playbook.yaml
- hosts: webservers
vars_files:
- vars/common.yaml
- vars/secrets.enc.yaml # Ansible will detect SOPS encryption
tasks:
- name: Deploy app with secrets
template:
src: app.conf.j2
dest: /etc/app/app.conf
# Use sops lookup in tasks
- name: Get secret from encrypted file
vars:
db_password: "{{ lookup('community.sops.sops', 'secrets/db.enc.yaml') | from_yaml | json_query('db.password') }}"
debug:
msg: "DB password is set"
# Ansible-vault vs SOPS comparison:
# ansible-vault: encrypts entire file, single password, no KMS
# sops: encrypts per-value, supports KMS/age/PGP, auditable (keys visible), Git-diff friendly
Terraform + SOPS
# Install the community sops provider
# versions.tf
terraform {
required_providers {
sops = {
source = "carlpett/sops"
version = "~> 1.0"
}
}
}
provider "sops" {}
# Use sops_file data source to read encrypted files
# main.tf
data "sops_file" "secrets" {
source_file = "secrets/production.enc.yaml"
}
resource "aws_db_instance" "main" {
identifier = "myapp-db"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 20
db_name = "myapp"
username = data.sops_file.secrets.data["db_username"]
password = data.sops_file.secrets.data["db_password"]
# Mark as sensitive to hide in plan output
lifecycle {
ignore_changes = [password]
}
}
# Access nested YAML keys
locals {
api_keys = yamldecode(data.sops_file.secrets.raw)
}
output "api_endpoint" {
value = local.api_keys.api.endpoint
sensitive = false
}
# Run terraform with SOPS (credentials must be available)
AWS_PROFILE=production terraform plan
AWS_PROFILE=production terraform apply
CI/CD Integration
GitHub Actions with SOPS
# .github/workflows/deploy.yaml
name: Deploy to Production
on:
push:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Configure AWS credentials via OIDC (no long-lived keys)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-sops
aws-region: us-east-1
# Configure GCP credentials via OIDC
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/123/locations/global/workloadIdentityPools/github/providers/github
service_account: [email protected]
# Install SOPS
- name: Install SOPS
run: |
SOPS_VERSION=$(curl -s https://api.github.com/repos/getsops/sops/releases/latest | jq -r .tag_name)
curl -LO https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.amd64
sudo mv sops-${SOPS_VERSION}.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
# Decrypt secrets and use them
- name: Deploy with decrypted secrets
run: |
# Decrypt secrets file
sops -d environments/production/secrets.enc.yaml > /tmp/secrets.yaml
# Install with helm-secrets
helm secrets upgrade --install myapp ./chart \
-f environments/production/values.yaml \
-f /tmp/secrets.yaml \
--namespace production \
--atomic --timeout 5m
# Cleanup
rm -f /tmp/secrets.yaml
GitLab CI with SOPS
# .gitlab-ci.yml
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl jq
- |
SOPS_VERSION=$(curl -s https://api.github.com/repos/getsops/sops/releases/latest | jq -r .tag_name)
curl -Lo /usr/local/bin/sops https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.amd64
chmod +x /usr/local/bin/sops
script:
- sops -d secrets/production.enc.yaml | helm upgrade --install myapp ./chart -f -
environment:
name: production
variables:
AWS_ACCESS_KEY_ID: $CI_AWS_KEY_ID # From GitLab CI/CD variables
AWS_SECRET_ACCESS_KEY: $CI_AWS_SECRET_KEY # Mark as "Masked"
AWS_REGION: us-east-1
only:
- main
Key Rotation
Important: When a KMS key is rotated or a team member with an age key leaves, you must re-encrypt all SOPS files with the new key set. Simply changing
.sops.yaml is not enough — existing files still use the old keys until re-encrypted.
# === Workflow: Adding a new key (e.g., new team member) ===
# 1. Add new age public key to .sops.yaml
# 2. Run updatekeys on all encrypted files
find . -name "*.enc.yaml" -exec sops updatekeys {} \;
# Or for specific files
sops updatekeys environments/production/secrets.enc.yaml
sops updatekeys environments/staging/secrets.enc.yaml
# === Workflow: Removing a key (team member leaving) ===
# 1. Remove the key from .sops.yaml
# 2. Re-encrypt all files (updatekeys re-encrypts DEK with remaining keys)
find . -name "*.enc.yaml" | xargs -I{} sops updatekeys -y {}
# -y flag auto-confirms without prompting
# === Workflow: AWS KMS key rotation ===
# 1. Create new KMS key
aws kms create-key --description "SOPS key v2"
NEW_KEY_ARN="arn:aws:kms:us-east-1:123456789012:key/new-key-id"
# 2. Update .sops.yaml with new key ARN
# 3. Re-encrypt all files
for f in $(find . -name "*.enc.yaml"); do
echo "Updating: $f"
sops updatekeys -y "$f"
done
# 4. Schedule old key for deletion (after all files are re-encrypted)
aws kms schedule-key-deletion \
--key-id OLD_KEY_ID \
--pending-window-in-days 30
# === Emergency access (break-glass) ===
# Keep a separate age key in a secure vault (HSM, password manager)
# Add it to .sops.yaml as a fallback — never remove it
# Document: "Break glass key: contact [email protected]"
Best Practices
Common Mistakes to Avoid:
- Committing unencrypted secrets to Git — use
git-secretsor pre-commit hooks to prevent this. - Using a single KMS key in one region — creates a single point of failure. Use multi-region keys or multiple key ARNs in
.sops.yaml. - Sharing the age private key — each team member should have their own age key. Use key groups to require multiple approvals for critical secrets.
- Forgetting to run
updatekeysafter adding/removing team members — existing files retain old keys until explicitly re-encrypted. - Storing age private keys in CI/CD environment variables unmasked — always mark secret variables as masked/protected.
Team Workflow Tips:
- Use
.sops.yamlwithpath_regexto automatically apply the right keys based on file location — no manual--kmsflags needed. - Pair SOPS with a pre-commit hook:
detect-secretsor a custom script that checks for plaintext secrets beforegit push. - In Kubernetes, prefer External Secrets Operator (ESO) or Sealed Secrets for runtime secret injection rather than decrypting in CI and pushing to the cluster.
- Document which key belongs to whom in a team wiki. When someone leaves, you know exactly which age/PGP key to remove and which files to
updatekeys. - Test decryption in CI as part of your pipeline validation — ensures keys are correctly configured before deploy time.
Pre-commit Hook to Prevent Plaintext Secret Commits
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
exclude: '\.enc\.yaml$' # Exclude already-encrypted SOPS files
# Custom hook: ensure all secrets files are SOPS-encrypted
- repo: local
hooks:
- id: check-sops-encryption
name: Check SOPS encryption
entry: bash -c 'for f in "$@"; do if ! grep -q "sops:" "$f"; then echo "ERROR: $f is not SOPS encrypted!"; exit 1; fi; done'
language: system
files: 'environments/.*secrets.*\.yaml$'
# Install pre-commit
pip install pre-commit
pre-commit install
pre-commit run --all-files # Test