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:
- One data key per file. All values in a document use the same data key.
- Unique IV per value. Each value gets a fresh 256-bit (32-byte) random IV. GCM also produces a 16-byte authentication tag.
- 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.
- Stored format.
ENC[AES256_GCM,data:<b64>,iv:<b64>,tag:<b64>,type:str]. Thetypefield restoresint,float,bool,bytes,timeandcommentvalues on decryption.
This keeps the benefits Mozilla wanted from hiera-eyaml:
- Meaningful diffs. Keys are plaintext, so
git diffshows which entries changed. With atextconvdriver, reviewers can see cleartext diffs locally. - Easier merges. Two people editing different values rarely conflict, unlike whole-file PGP blobs.
- Targeted access.
sops decrypt --extractandsops set/unsetwork 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
lastmodifiedtimestamp as AAD) and stored insops.mac. - By default the MAC covers all values, including values left unencrypted by
encrypted_regexor_unencryptedsuffixes. 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: 2with 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
mergekey (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:
- Resolve keys. CLI flags or environment variables win. Otherwise the first matching
.sops.yamlcreation rule supplies the recipients and options. - Generate the data key. 32 random bytes from a CSPRNG.
- 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.
- Encrypt values. Each selected leaf is encrypted with AES-256-GCM, a fresh IV and the key path as AAD.
- Compute the MAC. SHA-512 over the values, encrypted with the data key.
- Write metadata. The
sopsblock records every encrypted data key,lastmodified,mac, selection options andversion.
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¶
- Read metadata. Parse the
sopsblock to find the encrypted data keys (or key groups). - 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-orderorSOPS_DECRYPTION_ORDERto 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. - Decrypt values. Each
ENC[...]value is decrypted with AES-256-GCM, its IV and its path as AAD. - Verify the MAC. Recompute the SHA-512 digest and compare it with the decrypted
sops.mac. Abort on mismatch. - Emit the document. Output plaintext without the
sopsblock (or open the editor forsops 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.yamlit finds (not the directory of the target file).--configorSOPS_CONFIGoverrides this. A.sops.ymlis not picked up; SOPS warns about it since 3.10.0. - Matching. Rules are evaluated top to bottom and the first match wins.
path_regexis matched against the file path relative to the config file. A rule withoutpath_regexmatches everything, so it belongs last as a catch-all. There is no exact-name matcher. The oldfilename_regexkey was replaced bypath_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 updatekeysre-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: sopsis 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
sopsbackend. 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 decryptand pipes the result tokubectl applyor 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:
- 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.
- 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. - 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¶
- SOPS documentation: encryption protocol, key groups, config file, security and threat model
- SOPS source:
sops.go(MAC),aes/cipher.go(32-byte nonce),config/config.go - SOPS CHANGELOG
- age README
- Flux Kustomization decryption and kustomize-controller CHANGELOG
- KSOPS and helm-secrets
- CNCF TOC health review for SOPS (#2098)