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-secrets or 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 updatekeys after 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.yaml with path_regex to automatically apply the right keys based on file location — no manual --kms flags needed.
  • Pair SOPS with a pre-commit hook: detect-secrets or a custom script that checks for plaintext secrets before git 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

Back to Documents