One of the most persistent questions in GitOps is: “How do I handle secrets?” Git repositories are often shared, sometimes public, and definitely logged. Putting plaintext database passwords in your repository is obviously wrong. But your deployment configuration lives in Git, and your deployments need secrets.
The approaches:
- External secrets management (Vault, AWS SSM, Azure Key Vault, Google Secret Manager): Secrets live outside Git, a Kubernetes operator pulls them at runtime.
- Sealed Secrets: A controller encrypts secrets using a cluster-resident key; the encrypted form is stored in Git.
- SOPS: Secrets are encrypted in your Git repository using age or PGP keys. The encrypted file is committed to Git; the deployment tool decrypts at apply time.
For homelab and SMB production use, SOPS is the most elegant solution. It’s simple, requires no external infrastructure, and integrates natively with Flux.
How SOPS Works
SOPS is a tool that encrypts YAML/JSON/ENV/INI files. It’s smart about what it encrypts: in a YAML file, it encrypts the values but leaves the keys unencrypted. This means you can see that a file contains database_password without seeing what the password is.
# Before SOPS encryption
database_password: mysecretpassword123
api_key: sk-abcdefghijklmnop
# After SOPS encryption (simplified)
database_password: ENC[AES256_GCM,data:xyz123==,iv:abc,aad:,tag:def==]
api_key: ENC[AES256_GCM,data:ghi456==,iv:jkl,aad:,tag:mno==]
sops:
kms: []
age:
- recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
<encrypted SOPS data key>
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-02-22T10:00:00Z"
mac: ENC[AES256_GCM,data:mac==,...]
version: 3.8.0
The file is committed to Git in this encrypted form. When Flux encounters a SOPS-encrypted file, it decrypts it using the key it has access to and applies the plaintext values as a Kubernetes Secret.
Setting Up age (The Recommended Encryption Method)
age is the preferred key type—simpler than PGP, modern cryptography, no key management complexity.
# Generate an age key pair
age-keygen -o key.txt
# Output:
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# The private key is in key.txt - protect this!
The private key (key.txt) is secret. The public key is safe to share and put in your .sops.yaml.
Configuring SOPS
Create .sops.yaml in your repository root:
creation_rules:
# All YAML files in secrets/ directories
- path_regex: .*/secrets/.*\.yaml$
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Specific secret files anywhere in the repo
- path_regex: .*secret.*\.yaml$
age: >-
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
age1backup987...
Using multiple age recipients means the same encrypted file can be decrypted by multiple keys—useful for team environments or when you need a backup key.
Encrypting Files
# Encrypt a file in place
SOPS_AGE_RECIPIENTS=age1ql3z7... sops --encrypt --in-place secret.yaml
# Or with .sops.yaml configured:
sops --encrypt --in-place secret.yaml
# Decrypt temporarily to edit
sops secret.yaml # Opens in $EDITOR, saves encrypted
# Decrypt to stdout (for piping)
sops --decrypt secret.yaml
Encrypted files go in Git. The private key stays out of Git.
Kubernetes Secrets with SOPS
For Kubernetes Secrets, create the YAML file and encrypt it:
# secrets/database.yaml (before encryption)
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
namespace: my-app
type: Opaque
stringData:
password: "mysecretpassword123"
connection-string: "postgresql://user:mysecretpassword123@postgres:5432/mydb"
sops --encrypt --in-place secrets/database.yaml
The encrypted file is committed. Flux decrypts and applies.
Flux Integration
Flux has native SOPS support. Configure it to use your age key:
# Store the private key as a Kubernetes Secret in flux-system
cat key.txt | kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=/dev/stdin
Tell Flux to use this key for decryption in your Kustomization:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app-secrets
namespace: flux-system
spec:
interval: 10m
path: ./clusters/production/my-app/secrets
prune: true
sourceRef:
kind: GitRepository
name: flux-system
decryption:
provider: sops
secretRef:
name: sops-age
When Flux processes files in ./clusters/production/my-app/secrets, it automatically decrypts any SOPS-encrypted files using the age key stored in sops-age.
Team Workflows
For teams, use multiple age recipients so multiple team members can decrypt:
# .sops.yaml
creation_rules:
- path_regex: .*secret.*\.yaml$
age: >-
age1alice...,
age1bob...,
age1cicd...
When Alice encrypts a file, both Bob and the CI/CD system can decrypt it. New team member? Add their public key to .sops.yaml and re-encrypt existing files:
# Re-encrypt all secrets with updated recipients
sops updatekeys secrets/database.yaml
When a team member leaves, remove their key and re-encrypt. Their old private key can no longer decrypt new secrets, and since you’ve re-encrypted existing secrets, their old key can no longer decrypt those either.
What SOPS Is Not
SOPS is not a secret rotation tool. It doesn’t integrate with identity providers for dynamic credentials. If you need:
- Automatic credential rotation
- Dynamic secrets that expire
- Integration with enterprise identity (LDAP, Active Directory)
- Multi-cloud secret synchronization
…then External Secrets Operator with Vault or a cloud secret manager is the right choice.
SOPS is for the common case: static secrets (TLS certs, API keys, database passwords) that don’t change frequently, in a GitOps repository, managed by a small team. For this use case, SOPS is the simplest possible approach that maintains security without requiring additional infrastructure.
Security Properties
SOPS with age provides:
- Confidentiality: Values encrypted with AES-256-GCM. Only key holders can decrypt.
- Integrity: MAC (message authentication code) protects against tampering. SOPS will refuse to decrypt a modified file.
- Key separation: The data encryption key is wrapped with the age key. You can add/remove recipients without re-encrypting the data.
- Audit trail: All changes to the encrypted secret file are tracked in git history.
What SOPS doesn’t provide:
- Access logging (who decrypted what, when)
- Fine-grained key access (if you can decrypt any secret encrypted with a key, you can decrypt all of them)
- Key rotation automation
For regulated environments needing access logging and fine-grained control, use Vault. For everyone else, SOPS is exactly enough security without excess complexity.