Have you ever needed to share a secret with a teammate and ended up pasting it into a chat window, promising yourself you would clean it up later? If that sounds familiar, this one is for you. Let's look at how SOPS works, and how to use it with the key stores teams actually run in production: age, AWS KMS, and HashiCorp Vault.
TL;DR
- What it is: SOPS encrypts the values inside YAML, JSON, ENV, and INI files and leaves key names readable, so encrypted secrets can live in Git.
- Key stores: AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS, age, and PGP, plus HashiCorp Vault/OpenBao, with rules in
.sops.yamlrouting each file to the right key. - What and when to use: age for local development and CI, cloud KMS or Vault for production, where IAM policies decide who can decrypt.
- Automation: GitHub Actions, Flux CD, helm-secrets, and KSOPS decrypt at build or reconcile time, so no plain text secret is committed.
- The limit: SOPS is not a secrets manager. Pair it with one for runtime access, and with secrets detection for leaks outside encrypted files.
What Is SOPS?
First, let's cut to the chase and address the elephant in the room: what is SOPS?
Because an image is worth a thousand words, here it is illustrated (kudos to @sec_r0 for the artwork):

SOPS, widely known as Mozilla SOPS, is an encrypted files editor. It handles encryption, decryption, and editing in one tool. The encryption keys stay in a key store of your choice: PGP (Pretty Good Privacy) / GnuPG, age, Amazon Web Services Key Management Service (AWS KMS), Azure Key Vault (Azure KMS), Google Cloud KMS, HashiCorp Vault / OpenBao, and HuaweiCloud KMS.
Without SOPS, encrypting a configuration file is a two-tool job. You write the file in an editor, then encrypt it with a separate tool. To read it, you decrypt it first, open the decrypted copy, and remember to delete it afterward. Every step leaves an unencrypted copy on disk for a while, not to mention you'd have to manage the encryption keys all by yourself, which seems to be OK but is not scalable once you have more than one person on the team.
Enter SOPS.
It collapses this into one command. Run sops secrets.yaml, edit the decrypted content in your editor, save, and SOPS re-encrypts the content before writing the file back, so the file itself is never stored in plain text. While the editor is open, the plain text lives in a temporary file that SOPS creates readable only by you and deletes when you quit. Anyone who opens the encrypted file with a normal text editor sees ciphertext and metadata. Anyone with access to the right key gets the original content back.
How SOPS Encryption Works
SOPS uses envelope encryption. Each file gets its own random data key, and that data key encrypts the secret values with AES-256-GCM. One or more master keys from your chosen key store then encrypt that data key: a PGP key, an age public key, a KMS key ARN, or a Vault transit key. A sops metadata block inside the same file records everything needed to decrypt it later, including which master keys were used. There is no separate file to track.
For structured formats, SOPS encrypts only the secret value and leaves the key name in plain text. A file that starts like this:
db_credentials:
username: app
password: hunter2looks like this after SOPS encryption:
db_credentials:
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-----
...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-08-14T09:12:41Z"
mac: ENC[AES256_GCM,data:...,type:str]A reviewer can see that password changed in a pull request without seeing the secret value. This per-value secret encryption is what makes SOPS encrypted secrets practical to keep in the same repository as application code and Kubernetes manifests.
Two details are worth knowing: SOPS also encrypts YAML comments, because a comment next to a secret often describes it; and SOPS stores a message authentication code (MAC) over the file's values, including the ones left in plain text, so a tampered file fails to decrypt instead of silently returning bad data. If you only want the MAC to cover the values SOPS actually encrypted, pass --mac-only-encrypted.
Supported File Formats and Key Management Services
SOPS supports YAML, JSON, ENV (dotenv), INI, and binary formats, which covers most configuration files a team needs to protect. For YAML, JSON, ENV, and INI it encrypts values and keeps keys readable. For any other file type, it treats the content as binary and encrypts the whole file.
On the key side, SOPS supports multiple encryption methods, or key stores, and you can mix them within one repository:
You do not have to pick one key store for every file. The .sops.yaml configuration file, covered below, lets you route production files to AWS KMS, staging files to a set of age recipients, and everything else to a PGP key. Want to edit a text file with a local age key? No problem. Want a dotenv file encrypted by Vault? Also covered.
OK, enough theory. Let's install it and get a feel for how it works.
How to Install SOPS
On macOS, install SOPS with Homebrew, the package manager for macOS:
brew install sopsOn Linux and Windows, download the binary or package for your platform from the SOPS releases page on GitHub. On Debian and Ubuntu, this fetches and installs the latest .deb:
SOPS_VERSION=$(curl -s https://api.github.com/repos/getsops/sops/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
curl -LO "https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops_${SOPS_VERSION#v}_amd64.deb"
sudo dpkg -i "sops_${SOPS_VERSION#v}_amd64.deb"
sops --versionSOPS opens the editor that the SOPS_EDITOR environment variable points to, and falls back to EDITOR if SOPS_EDITOR is not set. To use VS Code, add export EDITOR="code --wait" to your ~/.bashrc or ~/.zshrc. There is also a community vscode-sops extension that decrypts and re-encrypts files inside VS Code. It is community-maintained, so test it on non-critical files first.
SOPS with PGP Keys
PGP is the oldest key store SOPS supports and the quickest way to see it work on a single machine. For new team setups, the SOPS maintainers recommend age instead (covered in the next section). The workflow is identical, so the PGP walkthrough is still the best place to learn the basics.
PGP vs GPG
PGP, GPG... confusing, I know, right? The two names get mixed up constantly. In short, GPG is a command-line tool that implements PGP:
- PGP: An encryption program that provides cryptographic privacy and authentication. It signs, encrypts, and decrypts text, email, files, directories, and whole disk partitions.
- GPG (GnuPG): A free implementation of the OpenPGP standard, RFC 4880. It encrypts and signs data and ships with a key management system.
Now that we have the confusing names out of the way, let's create a PGP key with the GPG CLI so SOPS has something to encrypt with.
Create a PGP Key with GPG
On macOS, install GnuPG with Homebrew:
brew install gnupgOn Linux, use your package manager. On Debian and Ubuntu:
sudo apt-get install gnupgGenerate a key (put your own name in KEY_NAME):
export KEY_NAME="Tiexin Guo"
export KEY_COMMENT="test key for sops"
gpg --batch --full-generate-key <<EOF
%no-protection
Key-Type: 1
Key-Length: 4096
Subkey-Type: 1
Subkey-Length: 4096
Expire-Date: 0
Name-Comment: ${KEY_COMMENT}
Name-Real: ${KEY_NAME}
EOFRun gpg --list-keys to see the generated key:
$ gpg --list-keys
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
[keyboxd]
---------
pub rsa4096 2026-09-27 [SCEAR]
65E898A6DF79A111C62B7F459A815FE156C67FAC
uid [ultimate] Tiexin Guo (test key for sops)
sub rsa4096 2026-09-27 [SEA]
50FADB58B9194C0444CE2C67FCFF6AFD55F5D6F9The long hexadecimal string under pub is the GPG key fingerprint. You can also print it with gpg --list-secret-keys "${KEY_NAME}". Keep the GPG key fingerprint handy; SOPS references PGP keys by fingerprint.
Configure SOPS with .sops.yaml
SOPS reads its configuration from a file named .sops.yaml. It looks in the directory of the file it is encrypting or decrypting and then in each parent directory, so a .sops.yaml in your repository root applies to every file in that repo. For this test, create one in your home directory:
creation_rules:
- pgp: >-
65E898A6DF79A111C62B7F459A815FE156C67FACReplace the fingerprint with the one you generated. That is the whole setup, and we are ready to go.
Encrypt a Text File
Run the following command to create a new file with SOPS:
sops a-text-file.txtYour editor opens. Type something (I used "hello world"), save, and quit. Now cat the file:
$ cat a-text-file.txt
{
"data": "ENC[AES256_GCM,data:XkNRIt2aVBk/9re/,iv:M6FlDAMPV001kbm1IPT6Uwp2N3olJVkFl6hyZWttKgQ=,tag:F/ih4jLOJsKAbAg+UkT8Fw==,type:str]",
"sops": {
"lastmodified": "2026-09-27T02:35:13Z",
"mac": "ENC[AES256_GCM,data:uP3ZfrZXw7X6w9wtgepst2CKKEyCxtjzYF4uV1EVXtiyfo6qmUpARrj6HAsO+stIMT+D3ov0dkmtPDhacTfhHp0urKerVlPhvq9JQbwrsn2Et4uCuqFL1XoNfEB4qhWMy85mdVPaeu5x7/OhIwhS4PybcG1FijYiKvBmrYRW9ns=,iv:DEQyzZ6YSEiOmwAOur/ADZvbItqibQOXkZ0ANHfrPXk=,tag:M2vUmZv4essfBZAQuSk4hw==,type:str]",
"pgp": [
{
"created_at": "2026-09-27T02:35:07Z",
"enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMA/z/av1V9db5AQ/7BPOFI4MirPnykuHJ5JkRRiTAhx/3WvOf5xBsi4WMEPyF\nhNa2KXY3RB1xUjGUrmRUSY0uHBT3uKToxr/qVW1in1cKf6agOEGt8xNxRqei7BQM\nzF/vRsUGMvE0nsBrLGymmX77hXpjmRnVTfJr/h8076lrOC4VpYx1G4J/OiSE4a0R\nLBe6gFmc8eDjZ5UpamNdICuPSBLESVlJFR6CJ+rnqUs6Gqgi9wL0BQve8xO74tkp\n6PUNhXYwdvSwimXCijNM/uMlxtx8OXA+drhWCydLH6zhTx+X4aWzuP5vMVWBziAR\nz016o8+QW9SdaJrLkKwveuMI9id3Yk0tA1euMJyz4uGhdshyaAoQKDTxeCCBA1jm\n5Eydyd01tn9TRKzLHc6VJkEZpCFwJ1W6om5L/CIbCH9BG5mEpch3ilvk2UNr7T7t\n3ylIeERchjV8KAkEuwQRcOWdUWsQucNxknEr422WkOAL9xPtBLtGM2jUkP3SfoON\nnIfmSZLUKoKesO/pkXHjyIv+pEPxPi9GWF6yQqJ+QhRTNjMc6X8OJbe4Xy3VfAcs\nclAq4DWF6CdSzWJC2HYJofAxfGo0aPACPTnipH6WHOozoj7iZhyyEOEGqNStT3dF\nPDQmfs+y+PAI7HqMVLB6SktnEtR+oTmA9BFyXBEJGuKLxBnuJ8TvEHKku7H1pr7U\naAEJAhBe+1E9DuB4dEA/YZEWoE8/tixnWRSF8hDOD+5Z+bjRzYfPzEGyn4R93L7+\npsgpbnooXrAh78Foed+tvw/pTZqIM+3BxklrT3uAlN9Wuy35VJsoWfWuWI2T1/GU\nk65gxduzDwn8\n=EkDN\n-----END PGP MESSAGE-----",
"fp": "65E898A6DF79A111C62B7F459A815FE156C67FAC"
}
],
"version": "3.13.3"
}
}Two things stand out in the output:
- The whole file is encrypted (the data field). A plain
.txtfile has no structure to preserve, so SOPS treats it as binary. - The encryption method and key fingerprint are stored in the same file as metadata.
To read it, run the same command again:
sops a-text-file.txtAnd we get our original content back. Notice that you did not tell SOPS which key to use. It can decrypt files without extra flags because the metadata block already says which PGP fingerprint encrypted the data key.
Encrypt Structured Data: YAML, JSON, ENV, and INI
Encrypting a whole text file is useful, but when the file contains structured data (YAML, JSON, INI, ENV), SOPS can do even better. Let's give it a try.
Create a YAML file:
sops a-yaml-file.yamlAdd two key-value pairs and save: username: tiexin password: test
Then cat it:
$ cat a-yaml-file.yaml
username: ENC[AES256_GCM,data:mZi0E9JN,iv:2diEIuR6i5NtpRAXESdZLshxiSsALljijqufnPTA1Uo=,tag:G4IzF1d47NVbWn+nV16riw==,type:str]
password: ENC[AES256_GCM,data:+wF+SQ==,iv:aThqVCSv0nmTGKeqCL9TK3DTuHknuKZPJWENaffYmYQ=,tag:gF2SpCPmfp1vIR4IyYVL9A==,type:str]
...(omitted)Only the values are encrypted, not the keys. The keys stay readable, so anyone who opens the file still understands its structure without learning a single secret value.
You can probably already guess why this feature got me excited the first time I saw it: Kubernetes secret manifests. After encryption you can still see every key in data and stringData, but not the values, which makes the file safe to commit and share. And I was not the first to think of it: the GitOps tool Flux CD relies on exactly this, decrypting SOPS-encrypted Kubernetes secrets inside the cluster. We cover the Kubernetes workflow, including the helm secrets plugin, later in this guide.
For YAML files, SOPS also encrypts comments. Sensitive values often come with sensitive descriptions, and encrypting both closes that gap.
So far everything ran against a local PGP key, which is fine for learning but rarely how teams run SOPS in a production environment. So let's try the key stores teams actually use: age, HashiCorp Vault, and AWS KMS.
SOPS with age Keys
age (always lowercase) is a small, modern file encryption tool with a compact key format. The SOPS project recommends age over PGP where possible. There is no keyring, no key server, and no expiration dates, and an age key pair fits in one text file. For local development, homelabs, and CI pipelines that need a single decryption secret, age is the simplest key store.
Generate an Age Key Pair
Install age and generate a key. For macOS users:
brew install ageDebian users:
sudo apt install agemkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txtage-keygen prints the public key and writes both halves of the pair to keys.txt. Treat that file like an SSH private key: it is the only thing that decrypts.
When decrypting a file, SOPS will look for a text file named keys.txt in a sops subdirectory of your user configuration directory:
- On Linux, it looks for
keys.txtin$XDG_CONFIG_HOME/sops/age/keys.txtand falls back to$HOME/.config/sops/age/keys.txtif$XDG_CONFIG_HOMEisn’t set. - On macOS, it looks for
keys.txtin$XDG_CONFIG_HOME/sops/age/keys.txtand falls back to$HOME/Library/Application Support/sops/age/keys.txtif$XDG_CONFIG_HOMEisn’t set.
You can override the default lookup by:
- setting the environment variable
SOPS_AGE_KEY_FILE; - setting the
SOPS_AGE_KEYenvironment variable; - providing a command to output the age keys by setting the
SOPS_AGE_KEY_CMDenvironment variable. This command can read the age recipient for which to return the private key from theSOPS_AGE_RECIPIENTenvironment variable.
So, on macOS, we can override with SOPS_AGE_KEY_FILE:
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txtIn CI, you can pass the private key content directly through SOPS_AGE_KEY instead of a file.
Now reference the public key in .sops.yaml. To let several team members decrypt the same files, list their public keys separated by commas. Each recipient's public key encrypts its own copy of the data key, and each person decrypts with their own private key:
creation_rules:
- age: >-
age12lycjufx6awzqv68439jm8hf24y6wz2nme3km465yrv6ga7ury8qmm09x8,
age1qe5lxzzeppw5k79vxn3872272sgy224g2nzqlzy3uljs84say3yqgvd0swEncrypt and Decrypt with the SOPS CLI
The editor workflow (sops secrets.yaml, demoed in the GPG section) is convenient for humans. Scripts and pipelines encrypt and decrypt files with explicit flags on the SOPS CLI instead:
# encrypt an existing plain text file into a new file
sops -e secrets.yaml > secrets.enc.yaml
# encrypt in place
sops -e -i secrets.yaml
# decrypt to stdout; nothing is written to disk
sops -d secrets.enc.yaml
# decrypt in place
sops -d -i secrets.enc.yaml
# change a single value without opening an editor
sops set secrets.enc.yaml '["db_credentials"]["password"]' '"new-value"'Since version 3.9, SOPS also accepts the longer sops encrypt, sops decrypt, and sops edit subcommands; both forms do the same thing. The sops set, sops unset, sops rotate, and sops filestatus subcommands used later in this guide arrived in the same release, so they need SOPS 3.9 or newer.
To run a program with decrypted values as environment variables, without ever writing a plain text file, use exec-env. It works best with flat files such as a dotenv file:
sops exec-env secrets.enc.env 'npm start'SOPS with HashiCorp Vault
What Vault Does
Vault is a secrets manager at its core, and it also provides encryption as a service through its transit engine. Typical use cases include centralized secret management, dynamic credentials, and key management. You can run the community edition in your own infrastructure or use the hosted HCP Vault service.
For SOPS, Vault acts as a key store: the transit engine holds the master key that encrypts each file's data key.
Set Up a Local Vault for Testing
The quickest way to get a Vault server for this tutorial is to use the Vault binary.
Install the Vault binary on macOS:
brew tap hashicorp/tap
brew install hashicorp/tap/vaultOn Debian/Ubuntu:
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vaultThen start a development server:
vault server -dev -dev-root-token-id root -dev-tlsNote: This starts a development server for local testing only; never run Vault in dev mode in a production environment.
Validate that it is up by exporting environment variables in a new terminal and checking the server's status:
$ export VAULT_ADDR=https://127.0.0.1:8200
$ export VAULT_CACERT='<value from the dev server output>'
$ vault status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 2.1.1
Build Date 2026-09-15T21:21:56Z
Storage Type inmem
Cluster Name vault-cluster-1ffa85f6
Cluster ID efc84b70-0a63-6014-1de4-dfe2fc53c691
HA Enabled falseThen enable a transit engine and create a key for SOPS. Creating a dedicated transit mount for SOPS keeps its policies separate from the rest of Vault:
$ vault secrets enable -path=sops transit
Success! Enabled the transit secrets engine at: sops/$ vault write sops/keys/firstkey type=rsa-4096
Key Value
--- -----
allow_plaintext_backup false
auto_rotate_period 0s
deletion_allowed false
derived false
exportable false
imported_key false
...(omitted)Encrypt a File with a Vault Transit Key
Next, let's edit a file using the key we just created:
sops --hc-vault-transit $VAULT_ADDR/v1/sops/keys/firstkey another-yaml-file.yamlWrite some YAML key-value pairs and save. If you open the file with a plain editor, you will see the Vault key path stored in the metadata block, alongside the encrypted values. To read it, run:
sops another-yaml-file.yamlAnd that's it: the process is exactly the same as with a PGP key. Only the key store changed. Rather than passing --hc-vault-transit every time, put the key path under hc_vault_transit_uri in the .sops.yaml creation rules, as shown in the configuration section below:
creation_rules:
- hc_vault_transit_uri: "https://127.0.0.1:8200/v1/sops/keys/firstkey"And you can specify different keys for different environments, for example:
creation_rules:
- path_regex: \.dev\.yaml$
hc_vault_transit_uri: "https://127.0.0.1:8200/v1/sops/keys/secondkey"
- path_regex: \.prod\.yaml$
hc_vault_transit_uri: "https://127.0.0.1:8200/v1/sops/keys/thirdkey"SOPS does not expand environment variables inside .sops.yaml, so write the full address rather than $VAULT_ADDR.
SOPS with AWS KMS
What AWS KMS Does
AWS KMS creates and controls cryptographic keys. Like Vault, it can create, store, and use encryption keys. Unlike Vault, it is not a secrets manager: AWS has a separate service, AWS Secrets Manager, for storing, rotating, and serving secrets.
Create an AWS KMS Key
Install the AWS CLI and configure it with credentials that can access KMS. Creating a key is then surprisingly easy:
aws kms create-key \
--tags TagKey=Purpose,TagValue=Test \
--description "Test key"Copy the ARN from the output, and similarly, we can configure it in .sops.yaml to use it by default:
creation_rules:
- kms: 'arn:aws:kms:ap-southeast-1:789985488518:key/916630de-2cae-4cbe-b609-4bd671d6d250'Encrypt a File with a KMS Key
sops aws-kms-encryption-example.yamlAlternatively, if you prefer not to set the KMS ARN permanently in .sops.yaml, you can export the ARN and use it one time:
export ARN=arn:aws:kms:ap-southeast-1:789985488518:key/916630de-2cae-4cbe-b609-4bd671d6d250
sops --kms $ARN aws-kms-encryption-example.yamlAs before, the ARN is stored in the file's metadata. Reading the file with SOPS requires no extra flags, only AWS credentials that are allowed to call kms:Decrypt on that key.
Restrict Decryption with Encryption Context and IAM Roles
KMS encryption context is a set of non-secret key-value pairs that describe the data. SOPS stores the context in the file's metadata, and KMS requires the same context on every decrypt call. That lets you write key policies which only permit decryption for a given application or environment.
Set the context when you create a file:
sops edit --encryption-context Environment:production,AppName:ExampleApp test.dev.yamlThen reference it in a KMS key policy. This statement allows the RoleForExampleApp IAM role to decrypt with the key only when the request's encryption context includes AppName:ExampleApp:
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/RoleForExampleApp"
},
"Action": "kms:Decrypt",
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:AppName": "ExampleApp"
}
}
}The same pattern works in CI: give the pipeline an IAM role with kms:Decrypt on the SOPS key and nothing else. SOPS reads the role's credentials from the environment, so there is no static key to store anywhere.
If you are not on AWS, the workflow is identical with GCP KMS (parameter --gcp-kms) and Azure Key Vault (--azure-kv). The SOPS official doc details each key store's identifier format.
Other Useful SOPS Features
Route New Files to Different Keys with .sops.yaml Creation Rules
Creation rules tell SOPS which key store and key to use for new files based on their path, so nobody has to pass --kms or --age flags by hand. Rules are evaluated in order, and the first path_regex match wins.
This configuration encrypts *.dev.yaml with one KMS key, *.prod.yaml with another, and falls back to a PGP key for everything else:
# evaluated sequentially
creation_rules:
- path_regex: \.dev\.yaml$
kms: "arn:aws:kms:us-east-1:111111111111:key/xxx-xxx-xxx"
- path_regex: \.prod\.yaml$
kms: "arn:aws:kms:us-east-1:111111111111:key/yyy-yyy-yyy"
# No path_regex: matches everything as a default catch-all rule
- pgp: "A2B73FB4DA0891B38EECD35B47991CD146C9C4BC"A rule can list several key stores at once (for example, KMS plus age) so that any one of the listed master keys can decrypt the file.
Encrypt Only Specific Keys with encrypted_regex
By default, SOPS encrypts every value in a structured file. Sometimes that is too much. A Kubernetes Secret manifest has apiVersion, kind, and metadata fields that are not secret, and encrypting them breaks tools that need to read the object type and name. Use encrypted_regex to encrypt only the keys that match:
creation_rules:
- path_regex: \.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1REPLACE_WITH_YOUR_PUBLIC_KEYThis is the pattern the Flux documentation uses for Kubernetes secrets. The inverse option, unencrypted_regex, encrypts everything except matching keys. The _unencrypted suffix you saw in the metadata block earlier does the same job per key: any key ending in _unencrypted stays in plain text.
Key Groups and Shamir Secret Sharing
By default, when a creation rule lists several master keys, SOPS encrypts the data key with each of them. Any one master key can decrypt the file. That is usually what you want, for example one master key in each of two AWS regions for resilience. Read more about envelope encryption in the AWS KMS documentation.
Sometimes you want the opposite: decryption should require keys from several parties. Key groups do that. SOPS splits the data key into fragments using Shamir's secret sharing, encrypts each fragment with the master keys in one group, and requires a threshold of groups to decrypt. Fun fact: that is the same algorithm Vault uses to split its own unseal key, and it is worth a read if you are curious.
creation_rules:
- path_regex: .*keygroups.*
key_groups:
# First key group
- pgp:
- fingerprint1
- fingerprint2
kms:
- arn: arn1
role: role1
context:
foo: bar
- arn: arn2
aws_profile: myprofile
# Second key group
- pgp:
- fingerprint3
- fingerprint4
kms:
- arn: arn3
- arn: arn4
# Third key group
- pgp:
- fingerprint5Create files as usual. SOPS splits the data key into three fragments, one per group, and encrypts each fragment with the master keys in that group. Override the default threshold with --shamir-secret-sharing-threshold or the shamir_threshold setting in the rule.
Rotate and Update Encryption Keys
Two commands handle key changes:
sops rotate -i secrets.enc.yamlgenerates a fresh data key and re-encrypts every value with it. Run it on a schedule, and whenever a key that could decrypt the file is compromised or no longer trusted.sops updatekeys secrets.enc.yamlre-encrypts the data key against the master keys currently listed in.sops.yaml. Use it after adding or removing a recipient, or after swapping a KMS ARN. It rewrites only the metadata block, so the diff stays small. It prompts you to confirm the change; pass-yin scripts.
The order matters when someone loses access. Remove their key from .sops.yaml, run sops updatekeys so the file's metadata no longer encrypts the data key to them, and only then run sops rotate -i. Rotating first re-encrypts the data key to every key still listed in the file, which at that point still includes theirs.
One thing neither command does: rotate the secret values themselves. If a plain text copy of a file was ever committed or shared, rotate the underlying credentials (the database password, the API key) and then re-encrypt. Rotating the encryption key alone does not undo an exposure.
Audit Who Decrypted What
SOPS can log every decryption to a PostgreSQL database: the time, the user running SOPS, and the file that was decrypted. To enable it, create /etc/sops/audit.yaml:
backends:
postgres:
- connection_string: "postgres://sops:sops@localhost/sops?sslmode=verify-full"You need to create the database and its credentials first, using the schema in the audit/schema.sql file of the SOPS repository; it also creates a sops role whose default password is sops, which you should change. The advanced usage page of the SOPS documentation covers the setup. The audit configuration path is not configurable, so /etc/sops/audit.yaml should be writable by root only. For cloud key stores, the KMS or Vault audit log is often the more useful record, because it captures every decrypt call regardless of which tool made it.
Publish Encrypted Files to S3 or Vault
Encrypting a file is half the job; the file still has to reach the people or systems that need it. Wouldn't it be nice if SOPS could upload it for you? It can: sops publish $file uploads a file to a destination configured in .sops.yaml:
destination_rules:
- s3_bucket: "sops-secrets"
path_regex: s3/*
recreation_rule:
pgp: A2B73FB4DA0891B38EECD35B47991CD146C9C4BC
- vault_path: "sops/"
vault_kv_mount_name: "secret/"
vault_kv_version: 2
path_regex: vault/*
omit_extensions: trueFiles under s3/ go to the sops-secrets bucket, decrypted and re-encrypted with the PGP key defined in recreation_rule. Files under vault/ are decrypted and written into Vault's KV store under secret/sops/. recreation_rule works the same way as creation_rules.
Encrypted Secrets in Git, CI/CD, and Kubernetes
Git Workflow: Readable Diffs and Pre-Commit Checks
Two small pieces of Git configuration make SOPS files easier to live with in a git repository.
First, tell Git how to diff them. SOPS reuses each value's initialization vector when the plain text has not changed, so a commit touches only the values you actually edited, plus the mac and lastmodified fields. That tells you which keys changed but shows the new values as unreadable ciphertext. A textconv filter decrypts locally before diffing; the encrypted file is still what gets committed:
# .gitattributes
*.enc.yaml diff=sopsgit config diff.sops.textconv "sops -d".gitattributes is committed, but the textconv command lives in .git/config, so every clone needs it; add --global if you want it everywhere. Leave diff.sops.cachetextconv off, because turning it on stores the decrypted output inside .git.
Second, stop plain text secrets from reaching the repository. The most common SOPS failure is not a broken cipher. It is a developer who runs sops -d -i, edits the file, and forgets to re-encrypt it before committing. A pre-commit hook that rejects any file in the secrets directory without an ENC[ marker catches that case.
It does not catch a password pasted into a Helm values file or an API key hardcoded in application code. GitGuardian's State of Secrets Sprawl 2026 report counted 28.6 million new hardcoded secrets in public GitHub commits in 2025, up 34% year over year. SOPS protects the secrets you know about. A secrets scanner such as GitGuardian's ggshield, run as a pre-commit hook or CI step, catches the ones that leak elsewhere in the repository. The two work well together.
Decrypt Secrets in GitHub Actions and Other CI Pipelines
CI is where SOPS pays off. The pipeline can decrypt secrets at build time, and the only thing you store in the CI system is the key that unlocks the data keys, never the secrets themselves.
With age, store the private key as a GitHub Actions secret and expose it through SOPS_AGE_KEY:
steps:
- uses: actions/checkout@v7
- name: Install SOPS
uses: mdgreenwald/mozilla-sops-action@v2.1.1
with:
version: v3.13.3
- name: Decrypt configuration
env:
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
run: sops -d config/secrets.enc.yaml > "$RUNNER_TEMP/secrets.yaml"Two details in that snippet. The installer action publishes immutable tags only, with no floating v2, so pin a full version or a commit SHA, and name the SOPS version explicitly: leaving it at the default latest costs a GitHub API call that is rate limited to 60 requests an hour on shared runner IPs. And the decrypted file lands in $RUNNER_TEMP rather than in the checkout, so it cannot be swept into a Docker build context, a cache, or an uploaded artifact.
With AWS KMS there is nothing to store at all. Give the workflow permissions: id-token: write, exchange the OpenID Connect (OIDC) token for an IAM role with a step such as aws-actions/configure-aws-credentials, and grant that role kms:Decrypt on the SOPS key; SOPS then picks up the short-lived credentials from the environment. This avoids long-lived cloud keys in CI, which is one of the core recommendations in our GitHub Actions security cheat sheet. The Azure and GCP key stores support the same federated identity pattern.
Whatever the key store, prefer decrypting to stdout or straight into a process over writing plain text files, and never echo decrypted output into logs:
sops exec-env config/secrets.enc.env './deploy.sh'For other CI systems (GitLab CI, Jenkins, CircleCI), the steps are the same: install the SOPS binary, provide the key through a masked environment variable or a cloud identity, and decrypt during the job.
Managing Secrets in a GitOps Workflow with Flux and helm-secrets
Kubernetes is where most teams meet SOPS, because Kubernetes secret manifests are the classic file you must not commit in plain text. Several tools build on SOPS for this:
- Flux CD has native SOPS support. You commit encrypted Kubernetes manifests and store a decryption key: an age or PGP private key held as a Kubernetes secret in the flux-system namespace, or a cloud KMS role through workload identity. Flux decrypts during reconciliation. No plain text manifest exists in Git or in CI. Two requirements catch people out: the key inside that secret has to follow Flux's naming convention (a
.agekeysuffix for age,sops.ascfor PGP,sops.vault-tokenfor Vault), and the secret has to live in the same namespace as the Kustomization that references it. Encrypted manifests are meant for kustomize-controller, so do notkubectl applythem yourself. The Flux SOPS guide walks through the setup. - helm-secrets wraps Helm so that
helm secrets installdecrypts SOPS-encrypted values files on the fly. The helm secrets plugin is the usual choice when Helm charts, rather than raw manifests, define your releases. Our guide on how to handle secrets in Helm covers it with examples, and the Helm continuous deployment tutorial shows how secrets fit into an Argo CD pipeline. - Argo CD does not ship SOPS support in core. The KSOPS Kustomize plugin and helm-secrets both work with it, but KSOPS is a KRM exec plugin, so it takes two steps: install the binary in the repo-server, and set
kustomize.buildOptions: "--enable-alpha-plugins --enable-exec"in theargocd-cmConfigMap. - sops-nix brings the same model to NixOS hosts: system secrets stay encrypted in the Nix configuration repository and are decrypted at activation time.
For the wider set of options, including External Secrets Operator and Sealed Secrets, see how to handle secrets in Kubernetes.
Repository Security Practices for SOPS-Encrypted Files
Encrypting the values does not make the repository safe to neglect, and repository security is where most SOPS setups drift. A short list of practices keeps SOPS encrypted secrets protected:
- Least privilege on keys, not only on the repository. Read access to the repo should not imply decrypt access. Scope cloud key policies, Vault policies, and age recipients to the people and workloads that need them.
- Branch protection and required review on
.sops.yamland on the secrets directories. A change to.sops.yamlcan silently add a new recipient. - A status check that fails on unencrypted secret files.
sops filestatusprints{"encrypted":false}but still exits 0, so parse its output, for examplesops filestatus secrets.enc.yaml | jq -e .encrypted. Grepping forENC[works as well, but neither check proves that everything sensitive is encrypted: withencrypted_regexin play, a file can containENC[and still hold plain text values. - Secret scanning on every push, so a plain text credential that lands outside the secrets directory is caught before it is merged.
- A backup of the age private key or PGP key somewhere other than the laptop that generated it. SOPS has no recovery path: if the only master key is lost, the files are unreadable.
- Periodic review of who can decrypt. When someone leaves, remove their key from
.sops.yaml, runsops updatekeys, thensops rotate, and finally rotate the credentials inside the files.
When to Use SOPS, and When You Need More
The Problem SOPS Solves
If you have followed along and run the commands yourself, you have earned a break. So, story time.
Years ago I worked on a large fintech project built around an OpenShift cluster. We used namespaces to separate environments and treated the cluster as the single source of truth for secrets. Updating a secret meant editing it in the OpenShift console, and Jenkins picked up changes automatically. All seemed fine. That worked until we had to share sensitive information with another team member or move a secret to another cluster. The only option then was to export the Kubernetes secret as a YAML file and hand it over.
That is a security problem and a productivity problem at once. What if the YAML reaches somebody who should not have it? What if the value changes in the cluster and every exported copy is now stale, with nobody knowing which copy is current?
Using native Kubernetes secrets as the single source of truth is not a best practice, and consuming secrets from outside the cluster is not simple either. Sooner or later you end up with secret files that need storing and sharing. SOPS encrypts those files with almost no overhead, which makes storing them in a git repo and sharing them with the right people safe.
Alternatives: git-crypt and Ansible Vault
Two tools overlap with SOPS and can be the better fit for narrow use cases:
git-cryptencrypts chosen files transparently on commit and decrypts them on checkout, so a repository can mix public and private content. It supports only PGP keys, which you generate and distribute yourself, and it does nothing outside Git.- Ansible Vault encrypts variables and files so passwords and keys are not visible in Ansible playbooks or roles. It works much like SOPS but only inside Ansible. You can also use SOPS with Ansible through the community sops lookup plugin.
For generic usage, SOPS is still king, especially when you want different encryption methods for different files.
SOPS Is Not a Secrets Manager or a Password Manager
SOPS encrypts files. Although it looks like a secret manager because technically it "manages" secrets, it does not:
- store secrets centrally
- serve them over an API
- issue short-lived credentials
- or track which application read which value
As your use cases expand you will hit those limits: at some point something needs API access to a secret, and SOPS is not built for that.
That is the job of a secrets manager. Tools such as HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, and Doppler offer flexible formats, access policies, integrations with CLIs and CI/CD systems, and automatic syncing into Kubernetes clusters. Many teams run both: SOPS for GitOps configuration that must live in Git, and a secrets manager for runtime secrets that applications fetch on demand. Our overview of secrets management tools compares the main options.
SOPS vs Vault: Which One Do You Need?
The question comes up often, and the answer is usually "both, for different things." Vault is a secrets manager: a running service that stores, serves, rotates, and revokes secrets, and authenticates every client that asks for one. SOPS is a file encryption tool: it protects secret values that live in files you version in Git, and it stops running the moment the command exits. They are not competing implementations of the same idea, so comparing them feature by feature only helps once you know which problem you have.
They do meet in one place: SOPS can use Vault's transit engine (or OpenBao's) as its key store, so Vault holds the master key that wraps each file's data key. In that setup Vault never sees your secrets, only the data keys.
| SOPS | Vault | |
|---|---|---|
| What it is | A CLI that encrypts values inside files | A server that stores and serves secrets |
| Where the secret lives | In your Git repository, encrypted | In Vault's storage backend, behind its API |
| How a workload gets it | Decrypted at build or reconcile time, then materialized as a file or env var | Fetched at runtime over the API, via an agent, the CSI driver, or an operator |
| Smallest unit of access | The file: if you can decrypt it, you get every value in it | The path: policies grant read on individual secrets per identity |
| Updating a value | Edit, commit, review, reconcile | Write to the API; consumers pick it up on their next read or refresh |
| Revoking access | Remove the key, updatekeys, rotate, then rotate the credential |
Revoke the token, lease, or policy; effective immediately |
| Dynamic credentials | No | Yes, with TTLs and automatic revocation |
| Audit trail | Only if you enable the PostgreSQL audit backend, or read your cloud KMS logs | Every request, per identity, out of the box |
| What you operate | Nothing | A highly available service: storage, unsealing, upgrades, backups |
| Main failure mode | Lose the only master key and the files are unreadable forever | Vault down or sealed and workloads cannot start |
| License | MPL-2.0, CNCF sandbox project | BUSL-1.1 since 2023 for the community edition; OpenBao is the MPL-licensed fork |
Reach for SOPS when the secret is configuration. It belongs in the same commit as the manifest that consumes it, it changes through pull requests, a GitOps controller applies it, and you want the whole state of an environment reconstructable from one repository. SOPS also wins when there is nothing to run a server on: a laptop, a homelab, an air-gapped build host, or a CI job that needs one credential and then exits.
Reach for a secrets manager when the secret is consumed at runtime by something that can prove who it is. Short-lived database credentials issued per pod, tokens you need to revoke in seconds rather than in a pull request, a per-application audit trail, or hundreds of services that should each read exactly one path: none of that is a file encryption problem, and building it on top of SOPS gets clunky fast.
One asymmetry deserves emphasis, because it is the part teams discover late. A secret committed to Git is committed forever. Rotating the SOPS data key re-encrypts the current file; it does nothing about the ciphertext sitting in every past commit, every fork, and every developer's clone. If the key that decrypts those commits ever leaks, the history leaks with it. With Vault, deleting a secret or revoking a lease actually ends access. This is why the practical advice after any suspected exposure is to rotate the underlying credential, not just the encryption key.
Running both is the normal end state, and the two integration points are easy. Use Vault transit as the SOPS key store so no decryption key lives in Git or in CI, and use the External Secrets Operator or the Vault Secrets Operator to project Vault secrets into the cluster for the workloads that need runtime access. The dividing line I use: if a human changes it in a pull request, SOPS; if a workload fetches it at startup, the secrets manager.
What are SOPS secrets?
SOPS secrets are configuration values that have been encrypted with SOPS (Secrets OPerationS) and stored in a file, usually YAML, JSON, ENV, or INI. Only the values are encrypted; key names stay readable, and a metadata block in the same file records which master keys can decrypt it. Teams commit these encrypted files to Git and decrypt them locally or in CI with a PGP key, an age recipient's private key, a cloud KMS key, or a Vault transit key.
How secure is SOPS?
SOPS encrypts values with AES-256-GCM and protects each file's data key with the backend you choose, such as AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault, age, or PGP. Its security depends on how well you protect those master keys and who is allowed to decrypt with them. Keep the age private key or PGP key out of the repository, scope KMS and Vault access with least privilege, and rotate the underlying credentials if a plain text copy is ever exposed.
What is the difference between SOPS and Vault?
SOPS is a file encryption tool: it encrypts secrets inside files that live in your Git repository and decrypts them when needed. HashiCorp Vault is a secrets manager: it stores secrets centrally, serves them over an API, and can issue short-lived dynamic credentials. They are complementary rather than competing. SOPS can use Vault's transit engine as its key backend, and many teams keep GitOps configuration in SOPS while application runtime secrets come from a secrets manager.
How can I decrypt a SOPS file?
Run sops -d file.yaml to print the decrypted content to standard output, or sops -d -i file.yaml to decrypt in place. Running sops file.yaml without flags decrypts the file into your editor and re-encrypts it when you save. SOPS reads the master key information from the file's own metadata, so you only need access to one of the listed keys: a local age or PGP private key, or credentials for the KMS or Vault key.
What is age in SOPS?
age is a modern file encryption tool with a compact key format, and it is one of the key backends SOPS supports. An age key pair is a single text file: the public key (starting with age1) goes in .sops.yaml as a recipient, and the age private key stays with the person or system that needs to decrypt. The SOPS project recommends age over PGP for new setups because there is no keyring or key server to manage.
Can SOPS be used with Git?
Yes. Keeping encrypted secrets in Git is the main reason SOPS exists. Because only values are encrypted, git diff and pull requests still show which keys changed. Add a .gitattributes textconv rule so local diffs decrypt automatically, protect the secrets directory with a pre-commit check, and never commit the plain text output of sops -d.
How do you manage Kubernetes secrets with SOPS?
Encrypt the data or stringData fields of your Kubernetes Secret manifests with an encrypted_regex rule in .sops.yaml, commit the encrypted manifests, and let a GitOps tool decrypt them in the cluster. Flux CD supports SOPS natively through a decryption key stored in the flux-system namespace. For Helm-based deployments, the helm-secrets plugin decrypts SOPS-encrypted values files at install time, and Argo CD users typically rely on KSOPS or helm-secrets.
Which key management services does SOPS support?
SOPS supports PGP (through GnuPG), age, AWS KMS, GCP KMS, Azure Key Vault, and HashiCorp Vault's transit engine. You can combine several master keys on one file so that any one of them can decrypt it, or use key groups to require keys from multiple groups. Creation rules in .sops.yaml can route different file paths to different backends.
How does SOPS compare to git-crypt or Ansible Vault?
git-crypt encrypts whole files transparently on commit and checkout, but it supports only PGP keys and only works inside Git. Ansible Vault encrypts variables and files for Ansible playbooks and is tied to that ecosystem. SOPS encrypts individual values, supports cloud KMS and Vault backends alongside age and PGP, and works anywhere files are used, which makes it the more general choice for multi-environment and GitOps setups.
This article is a guest post. Views and opinions expressed in this publication are solely those of the author and do not reflect the official policy, position, or views of GitGuardian, The content is provided for informational purposes, GitGuardian assumes no responsibility for any errors, omissions, or outcomes resulting from the use of this information. Should you have any enquiry with regard to the content, please contact the author directly.



