SOPS and AGE: Practical Secrets Management in GitOps
Secrets encrypted in Git, per-environment keys, Flux automatic decryption. Our actual implementation: kubeconfig files in project root, SOPS .enc.yaml files, age keys in GitHub secrets.
By Jurg van Vliet
Published Nov 12, 2025
The Secrets in Git Problem
GitOps means infrastructure as code, versioned in git. But what about secrets—database passwords, API keys, TLS certificates?
The obvious answer: don't commit secrets to git. The practical problem: how do you deploy them? Manual secret management doesn't scale, breaks reproducibility, and creates drift.
SOPS (Secrets OPerationS) solves this. You can commit secrets to git—encrypted—while maintaining the GitOps workflow.
How SOPS Works
SOPS encrypts the values in YAML or JSON files while leaving the structure readable:
# secrets.enc.yaml (encrypted with SOPS)
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
namespace: production
stringData:
username: postgres
password: ENC[AES256_GCM,data:Qr8yLKJ...,iv:abc123...,tag:xyz789...,type:str]
database: clouds_of_europe
sops:
kms: []
age:
- recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB...
-----END AGE ENCRYPTED FILE-----
The structure is visible. You can see it's a Secret, see the keys (username, password, database), but the values are encrypted. Only systems with the decryption key can read the actual values.
AGE Keys for Encryption
We use age (Actually Good Encryption) for key management. Age is simpler than GPG—no key servers, no web-of-trust complexity, just a keypair.
Generating keys:
# Generate age keypair
age-keygen -o age.key
# Public key (for encryption): age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Private key (for decryption): stored in age.key
Store the private key somewhere secure. For local development, that might be ~/.config/sops/age/keys.txt. For CI/CD, GitHub secrets or equivalent. For Flux in-cluster, a Kubernetes secret.
Configuration with .sops.yaml
At the repository root, .sops.yaml defines encryption rules:
creation_rules:
# Test environment secrets
- path_regex: gitops/infrastructure/test/.*.yaml$
age: age1test...public-key-here
# Production environment secrets
- path_regex: gitops/infrastructure/production/.*.yaml$
age: age1prod...public-key-here
Different environments use different keys. If the test key is compromised, production secrets remain safe. This is per-environment isolation—the same principle as separate kubeconfig files.
Flux Integration
Flux can decrypt SOPS-encrypted files automatically. You configure a decryption secret once:
apiVersion: v1
kind: Secret
metadata:
name: sops-age
namespace: flux-system
stringData:
age.agekey: |
# created: 2025-01-15T10:30:00Z
# public key: age1ql3z...
AGE-SECRET-KEY-1ABCD...
Then reference it in Kustomization resources:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: infrastructure
namespace: flux-system
spec:
interval: 10m
path: ./gitops/infrastructure/production
prune: true
sourceRef:
kind: GitRepository
name: flux-system
decryption:
provider: sops
secretRef:
name: sops-age
Flux pulls encrypted manifests from git, decrypts them using the age key, and applies them to the cluster. You never handle plaintext secrets in CI pipelines.
Practical Workflow
Encrypting a new secret:
# Create plaintext secret
cat > database-credentials.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
stringData:
password: "actual-password-here"
EOF
# Encrypt with SOPS
sops -e database-credentials.yaml > database-credentials.enc.yaml
# Commit the encrypted version
git add database-credentials.enc.yaml
git commit -m "Add database credentials for production"
git push
Editing an existing secret:
# SOPS decrypts, opens in editor, re-encrypts on save
sops database-credentials.enc.yaml
# Commit the changes
git add database-credentials.enc.yaml
git commit -m "Rotate database password"
git push
Rule: Always encrypt before commit. We use pre-commit hooks to catch accidentally committed plaintext secrets.
Key Rotation
Rotating keys periodically limits the blast radius if a key is compromised:
# Generate new age key
age-keygen -o age-new.key
# Re-encrypt all secrets with new key
find gitops/ -name "*.enc.yaml" -exec sops rotate -i {} ;
# Update .sops.yaml with new public key
# Update Flux secret with new private key
# Commit and deploy
We rotate keys annually and when team members with key access leave.
What This Enables
Disaster recovery: Entire infrastructure from git, including secrets. Fresh cluster? Clone repo, bootstrap Flux, done.
Audit trail: Every secret change is a git commit. Who rotated the database password? Check git log.
Review process: Secret changes go through pull requests. Someone reviews before production.
No credential sprawl: No secrets in CI configuration, no secrets in chat history, no secrets on developer laptops (except the age key itself).
SOPS with age keys isn't the only secrets solution, but it's the one that makes GitOps actually work for complete infrastructure.
Sources:
#sops #age #secrets #gitops #security