Skip to content

Explanation

Scope

How SOPS works and why it is designed this way: envelope encryption, value-level encryption with AAD and a MAC, key groups with Shamir secret sharing, .sops.yaml rule resolution, key rotation semantics, the key service, GitOps decryption models, the threat model and project governance. Look-up tables live in Reference. Tasks live in How-to Guides.

Overview

SOPS (Secrets OPerationS) is an editor for encrypted files. It encrypts the values of structured files (YAML, JSON, ENV, INI) and leaves the keys in plaintext. A BINARY mode encrypts arbitrary files as one blob. The data key can be protected by any mix of age, PGP, AWS KMS, GCP Cloud KMS, Azure Key Vault, HashiCorp Vault or OpenBao Transit, and (since 3.12.0) HuaweiCloud KMS. SOPS was built at Mozilla in 2015 so that secrets could live in Git and be decrypted only on the target systems. It is a single Go binary with no server component, which is why it became the default "secrets in Git" tool for GitOps.

Architecture

SOPS is a CLI plus a Go library (github.com/getsops/sops/v3). The diagram below shows the main components that take part in an encrypt, decrypt or edit call.

flowchart LR
    subgraph CLI["sops CLI (cmd/sops)"]
        Cmd["Subcommands<br/>encrypt / decrypt / edit / rotate / updatekeys"]
        Cfg["Config lookup<br/>.sops.yaml or SOPS_CONFIG"]
    end

    subgraph Core["Core library"]
        Stores["Stores<br/>yaml / json / dotenv / ini / binary"]
        Tree["sops.Tree<br/>branches + metadata"]
        AES["aes cipher<br/>AES-256-GCM per value"]
        Shamir["shamir<br/>data-key splitting"]
    end

    subgraph KS["Key service layer"]
        LocalKS["Local keyservice<br/>(in-process)"]
        RemoteKS["Remote keyservice<br/>(sops keyservice, gRPC)"]
    end

    subgraph Keys["Master key sources"]
        Age["age<br/>X25519, SSH, plugins, PQ"]
        PGP["PGP / gpg-agent"]
        AWS["AWS KMS"]
        GCP["GCP Cloud KMS"]
        AZ["Azure Key Vault"]
        HCV["Vault / OpenBao Transit"]
        HW["HuaweiCloud KMS"]
    end

    Cmd --> Cfg
    Cmd --> Stores
    Stores <--> Tree
    Tree --> AES
    Tree --> Shamir
    Tree -->|"encrypt / decrypt data key"| LocalKS
    Tree -->|"--keyservice / SOPS_KEYSERVICE"| RemoteKS
    LocalKS --> Age
    LocalKS --> PGP
    LocalKS --> AWS
    LocalKS --> GCP
    LocalKS --> AZ
    LocalKS --> HCV
    LocalKS --> HW
    RemoteKS -.->|"same key sources on another host"| Keys
Component Role
Stores Parse and emit each file format. They turn documents into a sops.Tree and flatten or unflatten metadata (INI and dotenv have no nesting, so metadata is flattened)
Tree The document plus sops metadata. It walks leaves to encrypt or decrypt them and computes the MAC
aes cipher Encrypts each leaf with AES-256-GCM, a 32-byte random IV and the key path as AAD
shamir Splits the data key into one share per key group when key_groups are used
Key service A gRPC interface that encrypts or decrypts the data key with one master key. By default it runs in-process. sops keyservice exposes it on a socket so a remote machine can use keys it cannot see
Master key sources One package per backend (age/, pgp/, kms/, gcpkms/, azkv/, hcvault/, hckms/)

Envelope Encryption Model

SOPS uses a two-tier design. A random 256-bit data key encrypts the file content. One or more master keys each encrypt a copy of that data key. Several backends can then protect the same file without re-encrypting the data, and each encrypted copy is stored in the file's sops metadata under the backend's list.

flowchart TD
    subgraph MK["Master keys (envelope)"]
        KMS["AWS KMS key"]
        GKMS["GCP KMS key"]
        AKV["Azure Key Vault key"]
        AGE["age recipient"]
        PGP["PGP public key"]
        HV["Vault / OpenBao transit key"]
    end

    DK["Data key<br/>(256-bit random)"]
    FILE["Encrypted values<br/>ENC[AES256_GCM,...]"]
    META["sops metadata block"]

    KMS -->|"encrypts a copy of"| DK
    GKMS -->|"encrypts a copy of"| DK
    AKV -->|"encrypts a copy of"| DK
    AGE -->|"encrypts a copy of"| DK
    PGP -->|"encrypts a copy of"| DK
    HV -->|"encrypts a copy of"| DK

    DK -->|"AES-256-GCM per value"| FILE
    DK -->|"encrypted copies stored in"| META

    META -->|"sops.kms[]"| KMS
    META -->|"sops.gcp_kms[]"| GKMS
    META -->|"sops.azure_kv[]"| AKV
    META -->|"sops.age[]"| AGE
    META -->|"sops.pgp[]"| PGP
    META -->|"sops.hc_vault[]"| HV

By default any one master key can recover the data key. That gives both sharing (each person or system has its own key) and disaster recovery (an offline key survives the loss of a KMS key). The original Mozilla recommendation was two KMS keys in different regions plus one PGP key kept offline. Today an offline age key usually plays the PGP role.

Value-Level Encryption

Unlike whole-file tools (age, gpg, git-crypt), SOPS walks the document tree and encrypts each leaf value separately:

  1. One data key per file. All values in a document use the same data key.
  2. Unique IV per value. Each value gets a fresh 256-bit (32-byte) random IV. GCM also produces a 16-byte authentication tag.
  3. Key path as AAD. The concatenated key names leading to the value are used as AEAD additional data. A ciphertext moved to a different key fails authentication. This is also why YAML anchors are unsupported: they create dynamic paths.
  4. Stored format. ENC[AES256_GCM,data:<b64>,iv:<b64>,tag:<b64>,type:str]. The type field restores int, float, bool, bytes, time and comment values on decryption.

This keeps the benefits Mozilla wanted from hiera-eyaml:

  • Meaningful diffs. Keys are plaintext, so git diff shows which entries changed. With a textconv driver, reviewers can see cleartext diffs locally.
  • Easier merges. Two people editing different values rarely conflict, unlike whole-file PGP blobs.
  • Targeted access. sops decrypt --extract and sops set/unset work on one path.

An example of what is and is not encrypted in a Kubernetes Secret:

# Before encryption
apiVersion: v1
kind: Secret
metadata:
  name: myapp          # not encrypted when encrypted_regex is ^(data|stringData)$
data:
  username: YWRtaW4=   # value encrypted
  password: czNjcjN0   # value encrypted

# After: sops encrypt --encrypted-regex '^(data|stringData)$'
apiVersion: v1
kind: Secret
metadata:
  name: myapp
data:
  username: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
  password: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
sops:
  age:
    - recipient: age1...
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...encrypted data key...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2026-09-25T00:00:00Z"
  mac: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
  encrypted_regex: ^(data|stringData)$
  version: 3.13.3

Keys are not secret

SOPS assumes key names carry no sensitive information. Do not encode secrets into key names, file names or comments you exclude from encryption.

Message Authentication Code

Per-value AAD protects each ciphertext and its position. It does not detect a value that was added or removed. For that, SOPS computes a file-level MAC:

  • The MAC is a SHA-512 hash over the plaintext values of the tree, in document order.
  • The hash is encrypted with AES-256-GCM using the data key (with the lastmodified timestamp as AAD) and stored in sops.mac.
  • By default the MAC covers all values, including values left unencrypted by encrypted_regex or _unencrypted suffixes. Editing a plaintext field outside SOPS therefore breaks the MAC check.
  • With mac_only_encrypted: true (or --mac-only-encrypted, since 3.9.0) only encrypted values count. A fixed 32-byte initialization sequence is hashed first, so the two modes cannot be confused.

If the MAC does not verify, sops decrypt refuses to output data (--ignore-mac exists for recovery only).

Correction

Earlier versions of this page described the MAC as HMAC-SHA256 with a key derived from the data key. The implementation (sops.go, Tree.Encrypt) uses a SHA-512 digest that is then AES-GCM-encrypted with the data key.

Partial Encryption

Six mutually exclusive options choose which values to encrypt: unencrypted_suffix (default _unencrypted), encrypted_suffix, unencrypted_regex, encrypted_regex, unencrypted_comment_regex and encrypted_comment_regex. They can be set on the CLI or per creation rule. The chosen option is recorded in the file metadata so later edits behave the same way.

The trade-off is visibility versus integrity. Plaintext fields stay readable and diffable. Because the MAC covers them by default, they are still tamper-evident unless you enable mac_only_encrypted. The typical Kubernetes setting is encrypted_regex: ^(data|stringData)$, because Flux and most tools need apiVersion, kind and metadata in cleartext.

Partial encryption risks

Make sure excluded fields really are non-sensitive. If you enable mac_only_encrypted, changes to plaintext fields are no longer detected by SOPS. Rely on Git review for those. Since 3.11.0, selection options are ignored (with a warning) for BINARY files so they cannot accidentally leave the blob unencrypted.

Key Groups and Shamir Secret Sharing

Key groups turn "any one key decrypts" into a quorum. With several groups, SOPS splits the data key with Shamir's Secret Sharing. Each group gets one share, and every key in that group can decrypt the group's share.

flowchart LR
    DK["Data key"] --> SSS["Shamir split<br/>threshold k of n"]
    SSS --> S1["Share 1"]
    SSS --> S2["Share 2"]
    SSS --> S3["Share 3"]

    subgraph G1["Key group 1"]
        K1a["AWS KMS (us-east-1)"]
        K1b["AWS KMS (eu-west-1)"]
    end
    subgraph G2["Key group 2"]
        K2a["age: team A"]
        K2b["age: team B"]
    end
    subgraph G3["Key group 3"]
        K3a["age PQ: offline recovery"]
    end

    S1 -->|"encrypted to each key in"| G1
    S2 -->|"encrypted to each key in"| G2
    S3 -->|"encrypted to each key in"| G3
  • The default threshold (shamir_threshold: 0) equals the number of groups, so one key from every group is required.
  • shamir_threshold: 2 with three groups means any two groups suffice. This is useful when one group is an offline break-glass key.
  • On decryption, SOPS walks the groups in order and recovers one share per group until it has enough.
  • The merge key (since 3.9.0) concatenates key groups. This lets you reuse YAML anchors for common recipients.

Key groups provide separation of duty: no single team, cloud account or backend can decrypt alone. The cost is operational. Every decrypting system needs credentials for enough groups.

Encryption Flow

When SOPS creates or re-encrypts a file:

  1. Resolve keys. CLI flags or environment variables win. Otherwise the first matching .sops.yaml creation rule supplies the recipients and options.
  2. Generate the data key. 32 random bytes from a CSPRNG.
  3. Protect the data key. Split it with Shamir if there are several key groups, then ask the key service to encrypt the key (or share) with each master key.
  4. Encrypt values. Each selected leaf is encrypted with AES-256-GCM, a fresh IV and the key path as AAD.
  5. Compute the MAC. SHA-512 over the values, encrypted with the data key.
  6. Write metadata. The sops block records every encrypted data key, lastmodified, mac, selection options and version.
sequenceDiagram
    participant User
    participant SOPS as sops CLI
    participant Cfg as .sops.yaml
    participant KS as Local keyservice
    participant KMS as AWS KMS
    participant AGE as age library
    participant FS as File system

    User->>SOPS: sops encrypt -i secrets.prod.yaml
    SOPS->>Cfg: Find first creation rule matching the path
    Cfg-->>SOPS: kms ARN, age recipient, encrypted_regex
    SOPS->>SOPS: Generate 256-bit data key
    SOPS->>KS: Encrypt data key for each master key
    KS->>KMS: kms Encrypt (data key, encryption context)
    KMS-->>KS: CiphertextBlob
    KS->>AGE: Wrap data key to age recipient
    AGE-->>KS: age armored ciphertext
    KS-->>SOPS: Encrypted data keys
    SOPS->>SOPS: AES-256-GCM each selected value with path as AAD
    SOPS->>SOPS: SHA-512 MAC, encrypted with data key
    SOPS->>FS: Write values plus sops metadata block

Decryption Flow

  1. Read metadata. Parse the sops block to find the encrypted data keys (or key groups).
  2. Recover the data key. Master keys are tried in decryption order. The default is age,pgp (offline methods first), followed by all other backends. Set --decryption-order or SOPS_DECRYPTION_ORDER to change it, for example to try KMS first in CI. The first success wins. For key groups, one success per group is needed until the threshold is met.
  3. Decrypt values. Each ENC[...] value is decrypted with AES-256-GCM, its IV and its path as AAD.
  4. Verify the MAC. Recompute the SHA-512 digest and compare it with the decrypted sops.mac. Abort on mismatch.
  5. Emit the document. Output plaintext without the sops block (or open the editor for sops edit).

Errors from individual age identities are collected and reported only when decryption fails overall (since 3.11.0). One stale key in keys.txt therefore does not produce noise.

.sops.yaml Rule Resolution

The .sops.yaml file maps paths to recipients so that people do not need to pass --age or --kms every time.

  • Lookup. SOPS searches from the current working directory upwards and uses the first .sops.yaml it finds (not the directory of the target file). --config or SOPS_CONFIG overrides this. A .sops.yml is not picked up; SOPS warns about it since 3.10.0.
  • Matching. Rules are evaluated top to bottom and the first match wins. path_regex is matched against the file path relative to the config file. A rule without path_regex matches everything, so it belongs last as a catch-all. There is no exact-name matcher. The old filename_regex key was replaced by path_regex.
  • Precedence. Recipients given on the command line or in environment variables (SOPS_AGE_RECIPIENTS, SOPS_KMS_ARN, and others) cause the config file to be ignored for key selection.
  • Existing files. Creation rules apply when a file is created. For existing files, sops updatekeys re-applies the current rule's recipients.

Overly broad patterns are a real risk. A rule such as .*\.yaml$ placed before a production rule can encrypt production files with development keys, or the reverse. Anchored patterns (^secrets/prod/.*\.yaml$) and a deliberate catch-all at the end avoid this.

Key Rotation Semantics

SOPS separates who can decrypt from what the data is encrypted with:

Operation Changes master keys Changes data key Re-encrypts values Use when
sops updatekeys Yes, to match .sops.yaml No No Onboarding or offboarding a recipient, adding a backend
sops rotate Optionally (--add-*, --rm-*) Yes Yes Periodic hygiene, and after any key removal or compromise

Rotation vs updatekeys

updatekeys alone does not revoke access. A removed recipient who saved the old data key, or an old Git revision, can still decrypt. After a compromise, run updatekeys first (remove the key), then rotate (new data key). Only after that should you rotate the real credentials stored in the file. In the other order, the compromised key could see the new data key or the new credentials.

Git history is the limit of any rotation: old commits stay decryptable with old keys. Treat every value that was ever encrypted to a compromised key as exposed.

Key Service

All master-key operations go through a small gRPC API: "encrypt this data key with key X" and "decrypt this blob with key X". Requests carry key identifiers and data keys, never private key material. By default the service runs in-process. sops keyservice can run it on another machine (for example one with a YubiKey or PGP keyring), and --keyservice unix:///tmp/sops.sock (or tcp://...) forwards requests to it, much like gpg-agent forwarding. The connection has no authentication or encryption, so it must be tunneled (for example over SSH).

GitOps Decryption Models

SOPS has no server, so every GitOps integration decides where decryption happens and how the decrypting component gets keys.

sequenceDiagram
    participant Dev as Developer
    participant Git as Git repository
    participant SC as Flux source-controller
    participant KC as Flux kustomize-controller
    participant KMS as KMS / OpenBao / age Secret
    participant API as Kubernetes API

    Dev->>Dev: sops encrypt -i secret.yaml (encrypted_regex data, stringData)
    Dev->>Git: git push (ciphertext only)
    SC->>Git: Fetch revision
    SC-->>KC: Artifact (tarball)
    KC->>KC: kustomize build, detect sops metadata
    KC->>KMS: Decrypt data key (secretRef keys or workload identity)
    KMS-->>KC: Data key
    KC->>KC: Decrypt values, verify MAC
    KC->>API: Server-side apply plaintext Secret
  • Flux (native). kustomize-controller decrypts in-cluster when spec.decryption.provider: sops is set. Keys come from a Secret (*.agekey, *.asc, sops.aws-kms, sops.azure-kv, sops.gcp-kms, sops.vault-token) or from cloud workload identity. Recent kustomize-controller releases added object-level workload identity (.spec.decryption.serviceAccountName, v1.6.0), a global age key (v1.7.0), and in v1.9.0 (2026-06-17) OpenBao/Vault Kubernetes-auth token exchange plus age post-quantum support. Plaintext exists only in controller memory and in the resulting Kubernetes Secret.
  • Argo CD (plugin). Argo CD has no built-in SOPS support. Decryption happens in the repo-server during manifest generation, via KSOPS (a Kustomize exec plugin), helm-secrets (decrypts values files), or argocd-vault-plugin's sops backend. The repo-server must hold the keys, and rendered manifests (including plaintext Secrets) pass through Argo CD's manifest cache. That is a larger trust surface than Flux's model.
  • CI push. A pipeline runs sops decrypt and pipes the result to kubectl apply or Helm. This is simple, but the CI runner holds decryption rights for every environment it deploys.
  • Operators. Community operators (for example sops-secrets-operator) decrypt custom resources in-cluster for tools other than Flux.

Threat Model

The security of SOPS data is as strong as its weakest master key. Values use AES-256-GCM. Data keys are protected by the backend (KMS services use their own HSM-backed keys; PGP uses RSA or ECC; age uses X25519 or hybrid ML-KEM-768 + X25519).

Threat vector Impact Mitigation
Compromised cloud credentials grant use of a KMS key Decryption of every file encrypted to that key MFA, least-privilege key policies, encryption context, CloudTrail alerts
Compromised or mishandled PGP/age private key Decryption of all files for that recipient, including Git history Offline or hardware-backed keys (age-plugin-yubikey), key groups, updatekeys + rotate
Weak PGP key (for example 512-bit RSA) Factoring breaks the key SOPS enforces no minimum PGP key size. Prefer age
Tampered encrypted file Injected, removed or swapped values AAD binds values to paths. The MAC detects added or removed values
Stale recipients in metadata Former staff keep access Scheduled updatekeys, then rotate
CI/CD credential leak Attacker decrypts everything CI can decrypt Short-lived OIDC/workload identity, per-environment keys
Untrusted file points SOPS at an attacker-controlled Vault URL Data key or token exposure to that server SOPS_HC_VAULT_ALLOWLIST (3.13.0+)
Unauthenticated sops keyservice socket Anyone on the network can use your keys Bind to a Unix socket or tunnel over SSH
Plaintext left on disk after edit Local leak Temp file is owner-only (3.11.0). Temp files are removed on Ctrl+C/SIGTERM (3.12.2)
Harvest-now-decrypt-later (quantum) Future decryption of today's Git history age hybrid post-quantum recipients (age1pq1..., SOPS 3.12.0+)

.sops.yaml itself is not secret: key ARNs, resource IDs and age recipients are public identifiers. Access is gated by IAM or by possession of the private key.

age vs PGP

The maintainers and Flux both recommend age over PGP "if possible". PGP is not deprecated in SOPS 3.13.3.

Criterion age PGP
Key format One X25519 key pair (or SSH key, plugin, or PQ hybrid) Keyring with primary keys and subkeys
Key management One text file or env var per identity gpg-agent, trust database, keyservers
Cryptography X25519 + ChaCha20-Poly1305. Hybrid ML-KEM-768 + X25519 available RSA or ECC. Defaults depend on the GnuPG version
Attack surface Small library compiled into SOPS External gpg binary and agent
CI/CD SOPS_AGE_KEY environment variable Import key into a keyring in each job
Hardware keys Via plugins (for example age-plugin-yubikey) Smart cards / OpenPGP cards
Recommendation Preferred for new deployments Keep for existing files and PGP-centric organizations

Post-Quantum age Keys

age v1.3.0 added hybrid post-quantum keys (ML-KEM-768 combined with X25519). Identities start with AGE-SECRET-KEY-PQ-1 and recipients with age1pq1. SOPS accepts them natively since 3.12.0, and Flux kustomize-controller since v1.9.0. The main cost is size: a PQ recipient is about 2,000 characters, which makes .sops.yaml files and metadata much larger. The main benefit is protection against "harvest now, decrypt later" attacks on long-lived Git history.

Auditing

SOPS protects data at rest. It is not an access broker, so auditing comes from three places:

  1. Backend logs. Every KMS, Key Vault or Vault/OpenBao decryption is an API call with an identity, which the provider logs (CloudTrail, Cloud Audit Logs, Azure Monitor, Vault audit devices). This is the strongest audit story and a reason to prefer KMS or Transit for production.
  2. Optional SOPS audit log. SOPS can write a row (timestamp, OS user, file) to one or more PostgreSQL databases on every decryption, configured in the root-owned /etc/sops/audit.yaml. It only records decryptions on machines where it is configured, so it is not tamper-proof against a user with their own SOPS binary.
  3. Git. Commit history shows who changed which encrypted keys and when, but not who read them.

age and PGP decryptions leave no server-side trail at all.

Performance Considerations

SOPS work is proportional to the number of leaves plus one master-key operation per data-key recovery. Local backends (age, PGP) avoid network calls. Cloud KMS and Transit add one round trip per attempted key, which is why decryption order matters in CI. SOPS publishes no benchmarks. Earlier estimates on this page (milliseconds per file size and backend, seconds per secret for GitOps tools) had no documented test conditions and were removed. See Reference: Performance Characteristics.

History and Governance

Date Event
2015 Created at Mozilla by Adrian Utrilla and Julien Vehent. Later maintained by AJ Bahnken
2021-03 3.7.0 adds age support
2023-05-17 Accepted into the CNCF Sandbox. The repo moved from mozilla/sops to getsops/sops under new maintainers
2023-09 3.8.0, the first release from getsops
2026-03-17 CNCF TOC opened a project health review (cncf/toc#2098). The main concern is that SOPS's MPL-2.0 license is not an approved primary license for CNCF projects. Options listed: relicense, archive, or move to another foundation (Linux Foundation or OpenSSF). The issue was still open as of 2026-09

The project remains active (four 3.13.x releases between 2026-05 and 2026-07). The outcome of the health review is the main governance uncertainty for adopters. The file format and CLI are stable within v3, so a foundation change would not affect existing files.

Sources