PS Product SecurityKnowledge Base

๐Ÿ” Mozilla SOPS: age, KMS, and GitOps-Friendly Secret Workflows

Intro: SOPS is one of the cleanest ways to keep encrypted configuration in Git without normalizing long-lived plaintext secrets in the repository. The important design question is not โ€œcan we encrypt a YAML file?โ€ but who decrypts, where, with what root of trust, and for how long.

What this page includes

  • where SOPS fits relative to Vault, CI variables, and Kubernetes-native secret patterns;
  • installation, bootstrap, and day-one usage with age;
  • examples for .sops.yaml, file encryption, key rotation, and CI/GitOps usage;
  • sample outputs and review notes for teams adopting SOPS.

What SOPS is good at

SOPS is strongest when a team wants to:

  • keep encrypted YAML/JSON/ENV/INI files in Git;
  • let operators edit secrets without inventing a custom crypto workflow;
  • use age or cloud KMS as the root of trust;
  • preserve structure while encrypting leaf values rather than storing one giant opaque blob;
  • support GitOps or config-in-repo flows where plaintext should never be committed.

What SOPS is not

SOPS is not a full secret lifecycle platform. It does not replace:

  • dynamic short-lived secrets from Vault or cloud secret managers;
  • rotation orchestration for database credentials or cloud identities;
  • access governance, approvals, or audit flows by itself;
  • workload-identity design.

A practical mental model is:

  • use SOPS when encrypted config must live in Git;
  • use Vault / cloud secret managers when a runtime needs dynamic secret issuance, rotation, revocation, or brokered access.

Preferred keying choice

When possible, prefer age for simple modern file encryption. Use cloud KMS or Vault transit where the organization already has strong control ownership and audit around those services.

Installation patterns

Linux amd64 example

export SOPS_VERSION=v3.11.0
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
sops --version

macOS with Homebrew

brew install sops age
sops --version
age --version

Windows notes

Use the official release artifact or package manager the team standardizes on, then verify sops --version from PowerShell or a terminal. Keep the private age key in the standard user config path or a specifically controlled override path.

Day-one bootstrap with age

1. Generate an age key pair

mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt

Print the recipient (public key):

grep '^# public key:' ~/.config/sops/age/keys.txt

2. Create a .sops.yaml policy

creation_rules:
  - path_regex: secrets/.*\.ya?ml$
    age:
      - age1examplepublickeyreplacewithrealrecipient

3. Encrypt a file in place

sops encrypt -i secrets/app.enc.yaml

4. View or edit it safely

sops decrypt secrets/app.enc.yaml
sops edit secrets/app.enc.yaml

Example encrypted file shape

SOPS preserves document structure and adds metadata. A typical encrypted YAML looks like this:

apiVersion: v1
kind: Secret
metadata:
  name: app-config
stringData:
  DB_PASSWORD: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
  API_TOKEN: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
sops:
  age:
    - recipient: age1examplepublickeyreplacewithrealrecipient
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2026-03-29T12:00:00Z"
  mac: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
  version: 3.11.0

Useful commands

Encrypt to stdout

sops encrypt --age age1examplepublickeyreplacewithrealrecipient secrets/app.yaml > secrets/app.enc.yaml

Decrypt to stdout

sops decrypt secrets/app.enc.yaml

Decrypt from stdin with explicit type

cat secrets/app.enc.yaml | sops decrypt --input-type yaml --output-type yaml

Extract one value

sops decrypt --extract '["stringData"]["DB_PASSWORD"]' secrets/app.enc.yaml

Rotate data key metadata after changing recipients or policy

sops updatekeys -y secrets/app.enc.yaml

In-place edit using the configured editor

EDITOR=vim sops edit secrets/app.enc.yaml

Example .sops.yaml patterns

Simple age-based repo policy

creation_rules:
  - path_regex: secrets/.*\.ya?ml$
    age:
      - age1teamrecipientaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Different rules by path

creation_rules:
  - path_regex: env/dev/.*\.ya?ml$
    age:
      - age1devrecipientaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

  - path_regex: env/prod/.*\.ya?ml$
    age:
      - age1prodrecipientbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

KMS-backed example

creation_rules:
  - path_regex: prod/.*\.ya?ml$
    kms:
      - arn:aws:kms:us-east-1:111122223333:key/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee

Where the age private key lives

By default, SOPS looks for keys.txt in the SOPS config directory for the user profile. Keep that file out of Git, restrict file permissions, and prefer workstation storage or secret injection patterns that match the teamโ€™s trust model.

Useful overrides:

export SOPS_AGE_KEY_FILE=/secure/location/keys.txt
export SOPS_AGE_RECIPIENTS=age1examplepublickeyreplacewithrealrecipient

CI and GitOps usage notes

Safer CI pattern

  1. CI receives decryption authority from a workload identity or tightly-scoped cloud principal.
  2. The pipeline decrypts only in the stage that truly needs plaintext.
  3. Plaintext is written to ephemeral workspace only.
  4. Logs, artifacts, and debug output never include decrypted secret values.
  5. The job destroys workspace and short-lived credentials after use.

What not to do

  • do not store the private age key as a long-lived plaintext blob in the repository or base image;
  • do not decrypt in a wide early stage if only one later deploy step needs plaintext;
  • do not assume โ€œencrypted in Gitโ€ means runtime exposure is solved;
  • do not give every developer production recipients if the team has real environment separation.

Sample output you should expect

Version check

$ sops --version
sops 3.11.0

Extracting a field

$ sops decrypt --extract '["stringData"]["DB_PASSWORD"]' secrets/app.enc.yaml
super-secret-password-value

Missing key example

$ sops decrypt secrets/app.enc.yaml
Failed to get the data key required to decrypt the SOPS file.

Review checklist

  • .sops.yaml exists and matches repo layout intentionally
  • recipients differ by environment where that distinction matters
  • private keys or KMS auth are not baked into the repo or base image
  • CI decrypts only at the stage that needs plaintext
  • decrypted values do not land in artifacts, logs, shell history, or cache layers
  • operational ownership exists for recipient changes and updatekeys

Use this page with


Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.