SOPS Configuration Reference
Purpose: For platform engineers, documents SOPS secrets encryption configuration and usage in openCenter-gitops-base.
Type: Reference Audience: Platform engineers Last Updated: 2026-02-14
This document describes SOPS (Secrets OPerationS) configuration and usage in openCenter-gitops-base.
SOPS Overview
SOPS encrypts secrets at rest in Git repositories using age encryption. FluxCD automatically decrypts secrets during reconciliation.
Key Features: - Asymmetric encryption (age public/private keys) - Selective field encryption (encrypt only sensitive fields) - Git-safe encrypted files - FluxCD integration for automatic decryption
Age Key Generation
.sops.yaml Configuration
Common Configuration Patterns
Pattern 1: Encrypt Specific Fields Only
Encrypt only data and stringData fields in Kubernetes Secrets:
creation_rules:
- path_regex: \.yaml$
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
encrypted_regex: ^(data|stringData)$
Use case: Kubernetes Secret manifests where only sensitive fields need encryption
Pattern 2: Fully Encrypt Helm Values
Encrypt entire override values files in a consuming cluster repo:
creation_rules:
- path_regex: '^helm-values/.*\.ya?ml$'
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Use case: cluster-local Helm values files containing credentials or tokens
Pattern 3: Multiple Rules for Different Paths
Different encryption rules for different directories:
creation_rules:
# Cluster-local override values - fully encrypted
- path_regex: '^helm-values/.*\.ya?ml$'
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Kubernetes manifests - encrypt only selected fields
- path_regex: '^manifests/.*\.ya?ml$'
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
encrypted_regex: "^(data|stringData|credentials)$"
# SSH keys and kubeconfig - fully encrypted
- path_regex: '^(id_rsa|id_rsa\.pub|kubeconfig\.yaml|.*\.creds)$'
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Use case: Complex repository with different encryption requirements per directory
Pattern 4: Multiple Age Keys
Use different keys for different cluster repositories or environments. If a single consumer repo contains multiple environment roots, the rules can be path-based, for example:
creation_rules:
# Production repository or environment
- path_regex: '^clusters/prod/.*\.ya?ml$'
age: age1prod1234567890abcdefghijklmnopqrstuvwxyz1234567890abc
# Staging repository or environment
- path_regex: '^clusters/stage/.*\.ya?ml$'
age: age1stage1234567890abcdefghijklmnopqrstuvwxyz1234567890ab
# Development repository or environment
- path_regex: '^clusters/dev/.*\.ya?ml$'
age: age1dev1234567890abcdefghijklmnopqrstuvwxyz1234567890abcd
Use case: environment-specific repositories or cluster repos with separate encryption keys
SOPS Commands
Encrypt File
# Encrypt in place
sops -e -i secret.yaml
# Encrypt to stdout
sops -e secret.yaml > secret.enc.yaml
# Encrypt with specific age key
sops -e --age age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p secret.yaml
Decrypt File
# Decrypt to stdout
sops -d secret.yaml
# Decrypt to file
sops -d secret.yaml > secret.dec.yaml
# Decrypt in place (dangerous!)
sops -d -i secret.yaml
Edit Encrypted File
# Edit with default editor
sops secret.yaml
# Edit with specific editor
EDITOR=vim sops secret.yaml
FluxCD Integration
Kustomization with SOPS Decryption
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: cert-manager
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: cluster-services
path: ./services/cert-manager
prune: true
decryption:
provider: sops
secretRef:
name: sops-age
Kubernetes Secret Encryption
Before Encryption
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
namespace: app
type: Opaque
stringData:
username: admin
password: super-secret-password
connection-string: postgresql://admin:super-secret-password@db:5432/app
After Encryption (Selective)
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
namespace: app
type: Opaque
stringData:
username: ENC[AES256_GCM,data:YWRtaW4=,iv:...,tag:...,type:str]
password: ENC[AES256_GCM,data:c3VwZXItc2VjcmV0LXBhc3N3b3Jk,iv:...,tag:...,type:str]
connection-string: ENC[AES256_GCM,data:cG9zdGdyZXNxbDovL2FkbWluOnN1cGVyLXNlY3JldC1wYXNzd29yZEBkYjo1NDMyL2FwcA==,iv:...,tag:...,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-02-14T10:30:00Z"
mac: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
pgp: []
encrypted_regex: ^(data|stringData)$
version: 3.8.1
Helm Values Encryption
Before Encryption
# helm-values/override-values-v<chart-version>.yaml
replicaCount: 3
database:
host: postgres.example.com
port: 5432
username: app_user
password: my-secret-password
database: app_db
apiKeys:
stripe: sk_live_1234567890abcdefghijklmnop
sendgrid: SG.1234567890abcdefghijklmnopqrstuvwxyz
After Encryption (Full File)
replicaCount: ENC[AES256_GCM,data:Mw==,iv:...,tag:...,type:int]
database:
host: ENC[AES256_GCM,data:cG9zdGdyZXMuZXhhbXBsZS5jb20=,iv:...,tag:...,type:str]
port: ENC[AES256_GCM,data:NTQzMg==,iv:...,tag:...,type:int]
username: ENC[AES256_GCM,data:YXBwX3VzZXI=,iv:...,tag:...,type:str]
password: ENC[AES256_GCM,data:bXktc2VjcmV0LXBhc3N3b3Jk,iv:...,tag:...,type:str]
database: ENC[AES256_GCM,data:YXBwX2Ri,iv:...,tag:...,type:str]
apiKeys:
stripe: ENC[AES256_GCM,data:c2tfbGl2ZV8xMjM0NTY3ODkwYWJjZGVmZ2hpamtsbW5vcA==,iv:...,tag:...,type:str]
sendgrid: ENC[AES256_GCM,data:U0cuMTIzNDU2Nzg5MGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6,iv:...,tag:...,type:str]
sops:
age:
- recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-02-14T10:30:00Z"
mac: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
version: 3.8.1
Best Practices
Key Management
-
One key per cluster - Each cluster has its own age keypair
-
Backup keys securely - Store private keys in secure vault (1Password, Vault)
-
Rotate keys periodically - Rotate age keys annually or after compromise
-
Document key locations - Maintain inventory of which keys encrypt which clusters
Encryption Strategy
-
Encrypt at rest - All secrets encrypted before committing to Git
-
Selective encryption - Only encrypt sensitive fields when possible
-
Full file encryption - Encrypt entire Helm values files with secrets
-
Never commit plaintext - Use pre-commit hooks to prevent plaintext secrets
Troubleshooting
SOPS Cannot Find Age Key
Error: failed to get the data key required to decrypt the SOPS file
Solution:
# Set SOPS_AGE_KEY_FILE environment variable
export SOPS_AGE_KEY_FILE=${HOME}/.config/sops/age/${CLUSTER_NAME}_keys.txt
# Or use --age flag
sops -d --age $(cat ${HOME}/.config/sops/age/${CLUSTER_NAME}_keys.txt | grep "# public key:" | cut -d: -f2) secret.yaml
FluxCD Decryption Fails
Error: decryption failed: no age key found
Solution:
# Verify sops-age secret exists
kubectl get secret sops-age -n flux-system
# Recreate if missing
kubectl create secret generic sops-age \
--from-file=age.agekey=${HOME}/.config/sops/age/${CLUSTER_NAME}_keys.txt \
-n flux-system
Wrong Age Key Used
Error: no age key found for recipient
Solution:
# Check which age key encrypted the file
sops -s secret.yaml | grep age
# Update .sops.yaml with correct age key
# Then re-encrypt
sops updatekeys secret.yaml
Security Considerations
Key Storage
-
Private keys - Never commit to Git; store in secure vault
-
Public keys - Safe to commit in .sops.yaml
-
Kubernetes secrets - Protect sops-age secret with RBAC
Access Control
-
Age key access - Limit who can access private keys
-
Git access - Encrypted files still require Git access control
-
Kubernetes RBAC - Restrict access to sops-age secret
Alternative: Sealed Secrets
openCenter also supports Bitnami Sealed Secrets as an alternative to SOPS.
Comparison
| Feature | SOPS | Sealed Secrets | |---------|------|----------------| | Encryption | Age (asymmetric) | RSA (asymmetric) | | Decryption | FluxCD | Controller in cluster | | Key management | External (age keys) | Internal (cluster keys) | | Offline decryption | Yes (with private key) | No (requires cluster) | | Multi-cluster | One key per cluster | One key per cluster | | Rotation | Manual (updatekeys) | Automatic (controller) |