Secrets Management with SOPS and Age Encryption
Encrypt secrets in Git safely using Mozilla SOPS and age encryption for Kubernetes, Terraform, and CI/CD workflows.
What you'll learn
- ✓Why storing encrypted secrets in Git is a valid pattern
- ✓How SOPS encrypts values while keeping keys readable
- ✓Setting up age key pairs for team encryption
- ✓Integrating SOPS with Kubernetes and Flux
- ✓Rotating keys and managing access securely
Prerequisites
None — this post is self-contained.
Every team eventually faces the secrets problem: how do you store database passwords, API keys, and certificates so that your infrastructure code is reproducible without committing plaintext credentials to Git? Vault and cloud KMS services solve this at scale, but they require infrastructure of their own. Mozilla SOPS offers a lighter-weight alternative: encrypt secrets in place, store them in Git alongside your code, and decrypt them at deploy time.
Why SOPS
SOPS (Secrets OPerationS) encrypts the values in structured files (YAML, JSON, ENV, INI) while leaving the keys in plaintext. This means you can review a diff and see which secrets changed without seeing the actual values:
apiVersion: v1
kind: Secret
metadata:
name: api-credentials
stringData:
DATABASE_URL: ENC[AES256_GCM,data:8f3k2...,type:str]
API_KEY: ENC[AES256_GCM,data:j29dk...,type:str]
sops:
age:
- recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-07-02T10:00:00Z"
version: 3.9.0
You can see that DATABASE_URL and API_KEY exist, but the values are encrypted. The sops metadata block tells SOPS which keys can decrypt the file.
Setting Up Age Encryption
Age is a simple, modern encryption tool that replaces PGP for SOPS workflows. It is easier to use, has fewer footguns, and the key format is simpler.
Generate a key pair:
age-keygen -o keys.txt
# Output:
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Store the private key file securely. Each team member and each CI system gets its own key pair. The public keys go into the SOPS configuration; the private keys stay on the machines that need to decrypt.
Create a .sops.yaml configuration file in your repository root:
creation_rules:
- path_regex: .*\.secret\.yaml$
age: >-
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
age1an6grz70cesmtxlkt2kerdmz0re24yr8axr8apm5pq0verxmyxlq8n6dqy
- path_regex: environments/production/.*
age: >-
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
This configuration encrypts any file matching *.secret.yaml with two age recipients (two team members or systems can decrypt). Production secrets use a separate, restricted key.
Encrypting and Decrypting
Create a plaintext secrets file and encrypt it:
# Create the plaintext file
cat > db.secret.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
stringData:
username: admin
password: supersecretpassword123
host: db.internal.example.com
EOF
# Encrypt in place
sops --encrypt --in-place db.secret.yaml
SOPS reads the .sops.yaml rules, matches the filename, and encrypts the values using the specified age public keys. The file is now safe to commit.
To edit an encrypted file:
sops db.secret.yaml
SOPS decrypts the file into a temporary buffer, opens your editor, and re-encrypts when you save and close. You never see decrypted content on disk.
To decrypt for use in a script:
sops --decrypt db.secret.yaml | kubectl apply -f -
Integrating with Kubernetes and Flux
Flux, the GitOps toolkit, has native SOPS support. It decrypts secrets during reconciliation so you can store encrypted Kubernetes Secrets in your Git repository.
Configure the Flux Kustomization to use SOPS:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app-secrets
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: infrastructure
path: ./secrets
prune: true
decryption:
provider: sops
secretRef:
name: sops-age
Create the age private key as a Kubernetes Secret in the flux-system namespace:
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=keys.txt
Now Flux automatically decrypts any SOPS-encrypted file it finds in the ./secrets path and applies the plaintext Secret to the cluster. The decrypted values never touch Git.
Using SOPS with Terraform
SOPS works with Terraform through the sops provider:
terraform {
required_providers {
sops = {
source = "carlpett/sops"
version = "~> 1.0"
}
}
}
data "sops_file" "secrets" {
source_file = "secrets.secret.yaml"
}
resource "aws_db_instance" "main" {
engine = "postgres"
username = data.sops_file.secrets.data["db_username"]
password = data.sops_file.secrets.data["db_password"]
# ...
}
The secrets file is encrypted in Git. Terraform decrypts it at plan and apply time using the age key available on the machine.
CI/CD Integration
In GitHub Actions, store the age private key as a repository secret and make it available to SOPS:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SOPS
run: |
curl -LO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64
chmod +x sops-v3.9.0.linux.amd64
sudo mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops
- name: Decrypt and deploy
env:
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
run: |
sops --decrypt secrets.secret.yaml | kubectl apply -f -
The SOPS_AGE_KEY environment variable contains the private key content. SOPS reads it automatically without needing a key file.
Key Rotation
When a team member leaves or a key is compromised, rotate by adding the new key and removing the old one from .sops.yaml, then re-encrypt all affected files:
# Update .sops.yaml with the new key list
# Then re-encrypt all secret files
find . -name '*.secret.yaml' -exec sops updatekeys {} \;
The updatekeys command re-encrypts the data key with the updated recipient list without changing the secret values. Commit the re-encrypted files and the old key can no longer decrypt them.
Wrap-up
SOPS with age encryption gives you encrypted secrets in Git without the operational overhead of running a secrets management server. It integrates cleanly with Kubernetes, Flux, Terraform, and CI/CD pipelines. Start with age keys for simplicity, use .sops.yaml creation rules to enforce encryption policies, and rotate keys promptly when access changes. For larger organizations with dynamic secrets and lease management, complement SOPS with a tool like Vault, but for many teams, SOPS is the right balance of security and simplicity.
Related articles
- DevOps HashiCorp Vault for Secrets Management
Learn how to use HashiCorp Vault to store, access, and rotate secrets securely across your infrastructure and applications.
- CI/CD CI/CD Secrets Management Best Practices
Keep API keys, tokens, and database credentials safe in CI/CD with rotation, scoping, secret managers, and OIDC-based authentication.
- Kubernetes Kubernetes Secrets: Best Practices for Production
Practical patterns for managing Kubernetes Secrets safely: encryption at rest, external secret stores, RBAC scoping, rotation, and avoiding common leaks.
- DevOps GitOps with ArgoCD: Deploy Kubernetes Apps Automatically
Learn GitOps principles and deploy Kubernetes applications with ArgoCD. Covers installation, app configuration, sync policies, multi-environment setups, and rollbacks.