Skip to content

How-to Guides

Scope

Task recipes for SOPS 3.13.x: install, key setup, .sops.yaml, everyday file operations, cloud KMS and Vault/OpenBao backends, key groups, rotation, CI/CD, Flux, Argo CD, Terraform and troubleshooting. Commands use the subcommand syntax (sops encrypt, sops decrypt) introduced in 3.9.0. For flags, environment variables and schema details, see Reference. For how it works, see Explanation.

Setup

Install SOPS

# macOS / Linux (Homebrew)
brew install sops

# Linux: pinned release binary (check the checksum and cosign signature for production use)
SOPS_VERSION=v3.13.3
curl -LO "https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.amd64"
sudo install -m 0755 "sops-${SOPS_VERSION}.linux.amd64" /usr/local/bin/sops

# From source (requires Go >= 1.25)
go install github.com/getsops/sops/v3/cmd/sops@latest

# Container image
docker run --rm ghcr.io/getsops/sops:v3.13.3 --version --disable-version-check

Key Management

Create an age identity in the default location that SOPS searches:

brew install age   # or: apt install age

# Linux default path. On macOS the default is ~/Library/Application Support/sops/age/keys.txt
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
# "Public key: age1..." is printed. That recipient goes into .sops.yaml

# Show the public key again later
age-keygen -y ~/.config/sops/age/keys.txt

To keep the key elsewhere, point SOPS at it:

export SOPS_AGE_KEY_FILE=$HOME/secure/sops-age.txt
# or fetch from a password manager on demand
export SOPS_AGE_KEY_CMD="op read op://infra/sops-age/private-key"

To use AWS KMS instead of (or as well as) age, export the key ARNs. Two keys in different regions are recommended:

export SOPS_KMS_ARN="arn:aws:kms:us-east-1:111122223333:key/<key-id>,arn:aws:kms:eu-west-1:111122223333:key/<key-id>"

Generate Post-Quantum age Keys

Requires age >= v1.3.0 and SOPS >= 3.12.0 on every machine that decrypts.

age-keygen -pq -o ~/.config/sops/age/keys.txt
# Recipient starts with age1pq1... (about 2,000 characters). Use a YAML list in .sops.yaml

.sops.yaml Configuration

Put .sops.yaml at the repository root. Rules are evaluated top to bottom and the first match wins, so put specific rules first and the catch-all last. The valid key for AWS KMS is kms (not aws_kms).

# .sops.yaml (repository root)
creation_rules:
  # Production Kubernetes Secrets: KMS for automation + offline age for recovery
  - path_regex: ^clusters/prod/.*\.ya?ml$
    encrypted_regex: ^(data|stringData)$
    kms:
      - arn:aws:kms:us-east-1:111122223333:key/<prod-key-id>
    age:
      - age1qe5lxzzeppw5k79vxn3872272sgy224g2nzqlzy3uljs84say3yqgvd0sw

  # Staging: team age keys only
  - path_regex: ^clusters/staging/.*\.ya?ml$
    encrypted_regex: ^(data|stringData)$
    age:
      - age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c
      - age129h70qwx39k7h5x6l9hg566nwm53527zvamre8vep9e3plsm44uqgy8gla

  # dotenv files
  - path_regex: \.env$
    age: age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c

  # Catch-all (no path_regex = matches everything)
  - age: age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c

Precise patterns

Anchor patterns (^secrets/prod/.*\.yaml$). A broad rule like .*\.yaml$ placed early silently captures production files. SOPS finds .sops.yaml by searching upward from the current working directory, so run commands from inside the repository or pass --config.

Restrict who can edit the rules, and protect private keys:

chmod 644 .sops.yaml                       # readable by the team, changes go through review
chmod 600 ~/.config/sops/age/keys.txt      # private age identities

File Operations

Encrypt, Decrypt and Edit

# Encrypt in place (keeps the extension, so the store type is detected on decrypt)
sops encrypt -i secrets.yaml

# Encrypt to a new file
sops encrypt secrets.yaml > secrets.enc.yaml

# Decrypt to stdout, or in place
sops decrypt secrets.enc.yaml
sops decrypt -i secrets.yaml

# Edit: decrypt into $SOPS_EDITOR / $EDITOR, re-encrypt on save. Also creates new files from .sops.yaml
sops edit secrets.enc.yaml

# Different extension than the content type
sops decrypt --input-type json config.json.enc

# From stdin: --filename-override picks the creation rule and the store type
echo 'password: hunter2' | sops encrypt --filename-override clusters/prod/db.yaml > clusters/prod/db.yaml

The legacy flags sops -e, sops -d and sops -r still work in 3.13.3, but the subcommands are clearer and support --help per command.

Read or Change a Single Value

sops decrypt --extract '["data"]["password"]' secret.yaml
sops set secret.yaml '["data"]["password"]' '"bmV3LXBhc3N3b3Jk"'          # value is JSON
printf '%s' '"from-stdin"' | sops set --value-stdin secret.yaml '["api"]["token"]'   # 3.11.0+
sops unset secret.yaml '["data"]["legacy_key"]'
sops filestatus secret.yaml          # {"encrypted":true}

Encrypt Only Part of a File

# Kubernetes Secret: encrypt only data/stringData so Flux and kubectl can read metadata
sops encrypt -i --encrypted-regex '^(data|stringData)$' k8s-secret.yaml

# Leave selected keys readable
sops encrypt -i --unencrypted-regex '^(description|metadata)$' app.yaml

# YAML: encrypt only values preceded by a "# sops:enc" comment
sops encrypt -i --encrypted-comment-regex 'sops:enc' values.yaml

Only one selection option can be used per file. Setting it in the creation rule (encrypted_regex: ...) is less error-prone than CLI flags.

Show Cleartext Diffs in Git

echo '*.yaml diff=sopsdiffer' >> .gitattributes
git config diff.sopsdiffer.textconv "sops decrypt"
git diff    # decrypts both sides locally for display only

Pass Secrets to a Process Without Writing Files

# As environment variables for one command
sops exec-env secrets.env.yaml './migrate --db-url "$DATABASE_URL"'

# As a FIFO (read once, never on disk). {} is replaced with the path
sops exec-file secrets.json 'my-app --config {}'

# Drop privileges when running as root
sudo sops exec-env --user nobody secrets.yaml 'sh -c "./job"'

Configure Cloud KMS and Vault Backends

AWS KMS with Roles and Encryption Context

# Assume a role for the key: <KMS ARN>+<ROLE ARN>
sops encrypt -i --kms 'arn:aws:kms:us-east-1:111122223333:key/<key-id>+arn:aws:iam::111122223333:role/sops-prod' secrets.yaml

# Bind the ciphertext to an encryption context (stored in metadata, not needed at decrypt time)
sops encrypt -i --encryption-context Environment:production,Service:billing secrets.yaml

# Use a named AWS profile
sops encrypt -i --aws-profile prod secrets.yaml

Minimal key policy statement for a CI role. Add a kms:EncryptionContext:Environment condition to stop cross-environment use:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["kms:Decrypt", "kms:Encrypt", "kms:DescribeKey"],
      "Resource": "arn:aws:kms:us-east-1:111122223333:key/<prod-key-id>",
      "Condition": {
        "StringEquals": { "kms:EncryptionContext:Environment": "production" }
      }
    }
  ]
}

Encryption context is never added automatically. You must set it with --encryption-context or the context field of a kms entry in key_groups.

GCP Cloud KMS

gcloud auth application-default login
gcloud kms keyrings create sops --location global
gcloud kms keys create sops-key --location global --keyring sops --purpose encryption
sops encrypt -i --gcp-kms projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key secrets.yaml
# Optional: REST instead of gRPC, or a sovereign-cloud endpoint (3.12.0+ / 3.13.0+)
export SOPS_GCP_KMS_CLIENT_TYPE=rest

Azure Key Vault

az keyvault key create --vault-name my-vault --name sops-key --protection software --ops encrypt decrypt
# Key version can be omitted since 3.11.0 (latest is used on encryption)
sops encrypt -i --azure-kv https://my-vault.vault.azure.net/keys/sops-key/ secrets.yaml

Authentication uses DefaultAzureCredential: environment service principal, workload identity, managed identity, then Azure CLI.

HashiCorp Vault or OpenBao Transit

export VAULT_ADDR=https://vault.example.com:8200
vault secrets enable -path=sops transit          # OpenBao: bao secrets enable -path=sops transit
vault write -f sops/keys/prod type=aes256-gcm96
sops encrypt -i --hc-vault-transit "$VAULT_ADDR/v1/sops/keys/prod" secrets.yaml

# 3.13.0+: only contact known servers when decrypting files from other sources
export SOPS_HC_VAULT_ALLOWLIST="https://vault.example.com:8200/"

In .sops.yaml, use hc_vault_transit_uri: https://vault.example.com:8200/v1/sops/keys/prod.

Key Groups and Multi-Backend Redundancy

Multi-Backend Redundancy

Encrypt every production file to at least two independent master keys, so the loss of one backend (a deleted KMS key, account suspension, region outage) is survivable:

creation_rules:
  - path_regex: ^secrets/prod/.*\.yaml$
    kms:
      - arn:aws:kms:us-east-1:111122223333:key/<prod-key-id>     # CI/CD and automation
      - arn:aws:kms:eu-west-1:111122223333:key/<prod-dr-key-id>  # second region
    age:
      - age1qe5lxzzeppw5k79vxn3872272sgy224g2nzqlzy3uljs84say3yqgvd0sw  # offline recovery key

Require Several Teams to Decrypt (Key Groups)

creation_rules:
  - path_regex: ^secrets/prod/.*\.yaml$
    shamir_threshold: 2          # any 2 of the 3 groups. Omit to require all groups
    key_groups:
      - age:                     # Team A
          - age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c
      - age:                     # Team B
          - age129h70qwx39k7h5x6l9hg566nwm53527zvamre8vep9e3plsm44uqgy8gla
      - kms:                     # Platform KMS, with encryption context
          - arn: arn:aws:kms:us-east-1:111122223333:key/<prod-key-id>
            context:
              Environment: production
# Or manage groups on an existing file
sops groups add --file secrets.yaml --age age1... --kms arn:aws:kms:...
sops groups delete --file secrets.yaml 0
sops edit --shamir-secret-sharing-threshold 2 secrets.yaml

Key Rotation

Add or Remove a Recipient

# 1. Edit .sops.yaml (add or remove the age key / ARN)
# 2. Re-sync every encrypted file with its rule (keeps the data key)
sops updatekeys -y secrets/prod/*.yaml

Onboarding only needs updatekeys. Offboarding also needs a data-key rotation, because the removed person may have kept the old data key.

Rotate the Data Key

sops rotate -i secrets.yaml
# Rotate and change master keys in one step
sops rotate -i --add-age age1new... --rm-age age1old... secrets.yaml

Rotate periodically (for example yearly, and whenever someone with access leaves). Use a shorter cadence for highly sensitive files.

Respond to a Compromised Key

Order matters:

# 1. Remove the compromised key from .sops.yaml, then for every affected file:
sops updatekeys -y secret.sops.yaml     # revoke: compromised key loses its copy of the data key
sops rotate -i secret.sops.yaml         # new data key, all values re-encrypted
git commit -am "sops: revoke compromised key and rotate data keys"
# 2. Only now rotate the actual passwords/API keys stored inside the files

Values that were in Git history while the key was valid remain exposed. Rotate the real credentials.

Audit Which Keys Protect Your Files

sops updatekeys has no dry-run flag. Without -y it prints the planned changes and asks for confirmation, so answering n works as a diff:

# Show pending recipient changes per file (answer n to abort)
sops updatekeys secrets/prod/db.yaml

# List recipients referenced by encrypted YAML files without decrypting
grep -rhoE '(arn:aws:kms:[^ ]+|age1[0-9a-z]+|fp: [0-9A-F]+)' --include='*.yaml' secrets/ | sort | uniq -c

Also review CloudTrail, Cloud Audit Logs, Key Vault diagnostics or Vault audit devices for unexpected Decrypt calls.

CI/CD Integration

GitHub Actions with age

# .github/workflows/deploy.yml (fragment)
env:
  SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}   # identity in memory, never written to disk
steps:
  - uses: actions/checkout@v4
  - run: sops decrypt clusters/prod/app-secrets.yaml | kubectl apply -f -

CI with AWS KMS

Prefer short-lived credentials (GitHub OIDC to an IAM role, or IRSA/EKS Pod Identity) scoped to the KMS key policy above. If one file has KMS and age recipients, set SOPS_DECRYPTION_ORDER=kms,age so CI goes straight to KMS.

PGP in CI (Legacy)

# Avoid when possible. Prefer age
echo "$SOPS_PGP_KEY" | gpg --batch --import
sops decrypt secrets.yaml

GnuPG in ephemeral runners adds agent and keyring handling and a larger attack surface.

GitOps Integration

Flux Integration

# 1. Store the age identity in the cluster (the key name must end with .agekey)
cat ~/.config/sops/age/keys.txt |
  kubectl create secret generic sops-age \
    --namespace=flux-system \
    --from-file=age.agekey=/dev/stdin

# 2. Enable decryption on the Kustomization
flux create kustomization my-secrets \
  --source=GitRepository/flux-system \
  --path="./clusters/prod/secrets" \
  --prune=true \
  --interval=10m \
  --decryption-provider=sops \
  --decryption-secret=sops-age

The equivalent manifest:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: my-secrets
  namespace: flux-system
spec:
  interval: 10m
  path: ./clusters/prod/secrets
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  decryption:
    provider: sops
    secretRef:
      name: sops-age
    # With cloud KMS: omit secretRef and use workload identity instead.
    # Object-level identity needs the ObjectLevelWorkloadIdentity feature gate:
    # serviceAccountName: sops-kms

Other Secret keys Flux recognizes: *.asc (PGP), sops.aws-kms, sops.azure-kv, sops.gcp-kms, sops.vault-token. From kustomize-controller v1.9.0, OpenBao/Vault can use Kubernetes auth instead of a static token (--sops-vault-configmap). Do not kubectl apply SOPS-encrypted Secrets directly: that stores ciphertext in the cluster.

ArgoCD Integration

Argo CD has no native SOPS support. Pick one of these plugins and give the repo-server access to the keys.

KSOPS (Kustomize exec plugin):

# secret-generator.yaml
apiVersion: viaduct.ai/v1
kind: ksops
metadata:
  name: app-secrets
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: ksops
files:
  - ./secret.enc.yaml
---
# kustomization.yaml
generators:
  - ./secret-generator.yaml
---
# argocd-cm: allow exec plugins
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  kustomize.buildOptions: "--enable-alpha-plugins --enable-exec"

Install the ksops and kustomize binaries into argocd-repo-server (the KSOPS README uses an init container with viaductoss/ksops:v4.5.1). Mount the age key (for example SOPS_AGE_KEY_FILE pointing to a mounted Secret) or provide KMS credentials.

helm-secrets: encrypt Helm values files with SOPS and reference them as secrets://values.enc.yaml in the Application's valueFiles. See the helm-secrets ArgoCD guide.

argocd-vault-plugin: set AVP_TYPE: sops to fill <placeholder> values from a SOPS-encrypted file.

Terraform and OpenTofu

terraform {
  required_providers {
    sops = { source = "carlpett/sops" }
  }
}

# Data source: decrypted values end up in state, so use an encrypted remote backend
data "sops_file" "db" {
  source_file = "secrets/db.enc.yaml"
}

# Terraform >= 1.11 with provider >= 1.3.0: ephemeral resource keeps values out of state
ephemeral "sops_file" "db" {
  source_file = "secrets/db.enc.yaml"
}

Common Issues

Issue Diagnosis Fix
Failed to get the data key required to decrypt the SOPS file No listed master key is usable Check SOPS_AGE_KEY_FILE or the default keys.txt path, aws sts get-caller-identity, gcloud auth application-default print-access-token, VAULT_TOKEN
MAC mismatch. File has ..., computed ... File edited outside SOPS, or a bad merge Resolve conflicts with sops edit. For recovery only: sops decrypt --ignore-mac, then re-encrypt
Wrong keys used for a new file .sops.yaml rule order or path_regex, or run from the wrong directory Put specific rules first, anchor regexes, run from the repo root or pass --config
No creation rule applied, or rules ignored File named .sops.yml, or recipients passed via flags/env Rename to .sops.yaml. Unset SOPS_AGE_RECIPIENTS / SOPS_KMS_ARN
Secrets readable in Git encrypted_regex too narrow Check sops filestatus and the regex. Re-encrypt
Flux: failed to decrypt Missing .agekey suffix, wrong namespace, or metadata encrypted Recreate the Secret with the .agekey key in the Kustomization's namespace. Use encrypted_regex: ^(data\|stringData)$
Error on YAML with anchors Anchors and aliases are unsupported Expand anchors before encrypting
Error on top-level arrays SOPS needs a top-level sops key Wrap the array in a map key
Wrong MAC for YAML lists with comments on 3.13.2 Known regression Upgrade to 3.13.3
Slow decryption in CI Tries offline methods and multiple backends first Set SOPS_DECRYPTION_ORDER to the backend CI actually has

Sources