When database passwords, API keys, or TLS certificates are accidentally committed to Git repositories, cleaning up these sensitive data from history can be a significant operational burden. Keeping sensitive data separate from source code is a fundamental security rule; however, moving them to a centralized secret server (such as HashiCorp Vault) can be an overly complex infrastructure cost for small and medium-sized projects or lean GitOps flows. The combination of sops (Mozilla's Secrets OVerlay Provider tool) and the modern asymmetric encryption tool age provides a practical solution by encrypting the values in configuration files and keeping the structure visible, thus enabling secure version control on Git. sops supports YAML, JSON, ENV, INI, and BINARY formats.
In this guide, we will explore how to store configuration files in YAML, JSON, or ENV format encrypted in a Git repository using sops and age, including steps for team access and automatic decryption in CI/CD environments.
ℹ️ Why Non-Plain Text Configuration?
sopsencrypts only the values, not the entire file as a block. This way, the keys in YAML or JSON files remain readable. You can see which parameter has changed in Git commits, get diffs, and easily detect merge conflicts.
The Problem of Secret Storage in Git Repositories and the SOPS / Age Response
The most common method for excluding configuration files from Git is to ignore them using .gitignore. However, this approach leads to the sharing of environment variables required for the application among developers through insecure channels (Slack, email, chat software). Over time, it becomes impossible to track who gave which key to which environment. Additionally, in GitOps architectures, the principle of storing the infrastructure state in the Git repository as a single source of truth is compromised by the complete exclusion of sensitive data from repositories.
Alternative approaches to solving this problem have significant limitations:
- GPG / PGP-based tools (git-crypt, etc.): PGP key management is complex, and maintaining configurations and certificate trust chains is difficult.
git-cryptencrypts the file content, so even the smallest change in encrypted files requires Git to store the entire file, which can make diffing inefficient or make patching difficult. Also,git-cryptdoes not support retroactive access revocation. - Centralized Secret Vaults (HashiCorp Vault, AWS Secrets Manager): These are comprehensive and powerful solutions but require high availability (HA) setup, authentication policies, and maintenance costs. They are too heavy for small teams or independent projects.
The combination of sops and age offers a practical solution without creating a dependency on a centralized service, based on file-based and asymmetric key infrastructure. sops scans the configuration tree, leaves key names open, and encrypts only the values with a symmetric data encryption key (DEK), which is then enveloped (envelope encryption) with public keys produced by age using X25519 key exchange.
What is Age (Actually Good Encryption) and How to Generate Key Pairs?
age is a simple, modern, and secure file encryption tool designed by Filippo Valsorda to replace GPG. It does not carry the burden of complex configuration options or old cryptographic algorithms from the past. Behind the scenes, age uses X25519 key exchange, ChaCha20-Poly1305 symmetric encryption, and HMAC-SHA256. age keys are small, readable, and in an open format.
Generating an age key pair is extremely simple. After installing the tool on your system, you can generate your first key with the age-keygen command.
# Installation on macOS with brew
brew install age sops
# Installation on Linux (Debian/Ubuntu)
sudo apt-get install age
# Downloading sops binary (Linux x86_64)
# The current stable version is 3.13.1.
curl -LO https://github.com/getsops/sops/releases/download/v3.13.1/sops-v3.13.1.linux.amd64
sudo mv sops-v3.13.1.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
To generate the age key pair used for encryption and decryption, run the following command:
# Create the key directory
mkdir -p ~/.config/sops/age
# Generate the key pair and write it to a file
age-keygen -o ~/.config/sops/age/keys.txt
This command produces a keys.txt file containing two critical pieces of information:
# created: 2026-08-13T10:00:00Z
# public key: age1ql3z7hjy54pw302mvpyqy54pw302mvpyqy54pw302mvpyqy54pw3s7x883
AGE-SECRET-KEY-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQP9S8XYZ
- Public Key (
age1...starting): This public key can be shared with your team, Git repository, or configuration files. It is all that's needed to encrypt data. - Secret Key (
AGE-SECRET-KEY-1...starting): This secret key must never be committed to Git, must be kept secure, or added to CI/CD variables. It is the only component that can decrypt encrypted data.
⚠️ Private Key Security
If your
keys.txtfile is lost or deleted, it will be mathematically impossible to decrypt the configuration data encrypted with that key. It is recommended to keep a backup of the key in a secure password manager.
How SOPS (Secrets OVerlay Provider) Works?
sops applies the "Envelope Encryption" technique when processing files. Each time a file is encrypted, sops generates a random symmetric key (Data Encryption Key - DEK). All values in the file are encrypted with this DEK using AES256-GCM.
Then, this DEK is encrypted separately with the public keys of the intended recipients (defined in the configuration) and added to the file's sops metadata block.
# Example encrypted data structure
database:
host: ENC[AES256_GCM,data:10.0.0.12,iv:...,tag:...,type:str]
password: ENC[AES256_GCM,data:SecretPass123,iv:...,tag:...,type:str]
sops:
kms: []
age:
- recipient: age1ql3z7hjy54pw302mvpyqy54pw302mvpyqy54pw302mvpyqy54pw3s7x883
enc: -----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-08-13T10:15:00Z"
mac: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
version: 3.13.1 # Current sops version
In the example above:
-
database.hostanddatabase.passwordkeys are left open, but their values are encrypted inENC[...]format. - Under the
sopsblock, information about whichagerecipient was used and the enveloped DEK is stored. - MAC (Message Authentication Code): Protects the file's integrity. If someone tries to modify the unencrypted key names,
sopswill detect an integrity error during decryption.
Step-by-Step Setup and .sops.yaml Configuration
To avoid writing long command-line parameters (sops --age age1... -e config.yaml) every time, a rules file named .sops.yaml is added to the project root directory. This file determines which files or patterns are encrypted with which keys.
Create a .sops.yaml file in your project directory with the following content:
creation_rules:
# Rules for staging environment
- path_regex: secrets/staging/.*\.yaml$
age: >-
age1ql3z7hjy54pw302mvpyqy54pw302mvpyqy54pw302mvpyqy54pw3s7x883,
age1dev2345678901234567890123456789012345678901234567890123456
# More restrictive rule for production environment
- path_regex: secrets/production/.*\.yaml$
age: >-
age1ql3z7hjy54pw302mvpyqy54pw302mvpyqy54pw302mvpyqy54pw3s7x883,
age1prod987654321098765432109876543210987654321098765432109876
This configuration automatically authorizes the specified age public keys for any YAML file under secrets/staging/ or secrets/production/.
File Creation and Encryption Workflow
Now, let's create a configuration file and manage it with sops.
1. Creating a New Encrypted File:
You can directly open and edit a new file through sops. Your default text editor (defined by the EDITOR environment variable) will open:
export EDITOR=vim
sops secrets/staging/app-config.yaml
Enter and save the following data in the editor:
api_key: "sk_test_998877665544332211"
database:
url: "postgres://user:supersecret@10.0.0.5:5432/app_db"
pool_size: 20
After saving and exiting, sops will encrypt the values according to the rules defined in .sops.yaml and save the file encrypted on disk.
2. Encrypting an Existing Plain Text File:
To encrypt an existing config.yaml file in place:
sops -e -i secrets/staging/app-config.yaml
The -e (--encrypt) parameter indicates encryption, and -i (--in-place) means the result will be written directly back to the input file.
3. Reading and Decrypting an Encrypted File:
To view the encrypted file in plain text in the terminal:
# Set the environment variable with the path to your private key
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
sops -d secrets/staging/app-config.yaml
This command decrypts the file, but does not modify the file on disk.
💡 Usage with Kubernetes Secrets
sopsencrypts all values by default. However, in Kubernetes manifest files, you might want only thedataandstringDatasections to be encrypted, leavingapiVersion,kind, andmetadataopen. For this, you can addencrypted_regex: '^(data|stringData)$'to your.sops.yamlfile.
# Example .sops.yaml rule for Kubernetes manifests
creation_rules:
- path_regex: k8s/.*\.yaml$
encrypted_regex: '^(data|stringData)$'
age: "age1ql3z7hjy54pw302mvpyqy54pw302mvpyqy54pw302mvpyqy54pw3s7x883"
Integration with CI/CD and GitOps Pipelines
Encrypting files and storing them in Git is half the job. The other half is having CI/CD runners or GitOps operators (like ArgoCD or FluxCD) automatically decrypt these secrets during deployment.
GitHub Actions Integration
The following GitHub Actions workflow example decrypts a sops-encrypted YAML file during the workflow and performs application deployment:
name: Deploy Application
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Install SOPS and Age
run: |
# Current stable version is 3.13.1.
curl -LO https://github.com/getsops/sops/releases/download/v3.13.1/sops-v3.13.1.linux.amd64
sudo mv sops-v3.13.1.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
# Install age (may not be installed by default in GitHub Actions runners)
sudo apt-get update && sudo apt-get install -y age
- name: Decrypt Secrets and Deploy
env:
# Your AGE private key stored in GitHub Secrets
SOPS_AGE_KEY: ${{ secrets.MY_APP_AGE_PRIVATE_KEY }}
run: |
# Decrypt the encrypted file and produce a temporary configuration file
sops -d secrets/production/app-config.yaml > config.production.json
# Start or deploy your application
echo "Deployment started..."
# ./deploy.sh config.production.json
# Remove sensitive data when done
rm config.production.json
ArgoCD / FluxCD Integration
If you are using GitOps tools:
- FluxCD: It has built-in support for
sops. You define anageprivate key as aSecretobject in Kubernetes. Flux will automatically decrypt files during theKustomizationorHelmReleaseprocess if it encounters adecryption.provider: sopsdefinition. - ArgoCD: You can use the
ksopsextension or ArgoCD's ConfigManagementPlugin mechanism to rendersopsfiles into plain text during the deployment process.
Team Key Rotation and Multi-Recipient Strategy
Teams grow over time, new developers join, or members leave. When a developer leaves, it's necessary to change all secrets and re-encrypt them with new key pairs. sops' multi-recipient architecture makes this process easier.
Scenario 1: Adding a New Team Member
Let's say a new team member, Ahmet, has a public key age1ahmet....
- Add Ahmet's public key to the relevant rule in
.sops.yaml:
creation_rules:
- path_regex: secrets/.*\.yaml$
age: >-
age1mustafa...,
age1ahmet...
- Update the existing encrypted files with the new key list:
sops updatekeys secrets/staging/app-config.yaml
This command updates the file without changing the symmetric DEK, adding a new copy of the DEK enveloped with Ahmet's public key to the sops metadata area. Now, Ahmet can also decrypt this file with his private key.
Scenario 2: Key Rotation and Re-encryption
If a key is suspected to be compromised or due to periodic security requirements, both the symmetric data and recipient keys need to be completely renewed:
- Remove the old key from
.sops.yaml. - Re-encrypt all data with new symmetric keys using the
-r(--rotate) flag:
sops -r -i secrets/staging/app-config.yaml
This command decrypts the file, generates a new DEK, re-encrypts the data with the new DEK, and envelops it with only the currently valid recipient public keys defined in .sops.yaml.
Comparative Summary
The key characteristics of different secret management approaches are summarized in the following table:
| Feature | Plain Git + .gitignore | HashiCorp Vault | sops + age |
|---|---|---|---|
| Storage in Git | Not Possible | Not Possible | Fully Compatible |
| Diff / History Support | None | None | Value-level support |
| Infrastructure Dependency | None | High (Server/Cluster) | Zero-dependency binary |
| Learning Curve | Low | High | Low |
| Access Control (RBAC) | None | Very Detailed | Public/Private key-based |
| Cost | Free | Operational Cost | Free / Open Source |
Conclusion
The combination of sops and age is a practical way to secure configuration files in software development and infrastructure management processes. Without the operational burden of complex centralized secret servers, and without compromising on GitOps principles or Git commit history, you can securely version all sensitive data.
When integrating sops into your project, pay attention to the following key points:
- Add a valid
.sops.yamlrules file to your project root. - Never commit the generated
ageprivate keys (keys.txt) to your Git repository; store them securely in a password manager or CI/CD secret storage. - Update key lists with
sops updatekeyswhen team members change. - Define
SOPS_AGE_KEYenvironment variables in your CI/CD workflows for automatic deployment processes.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.