BlogSecrets

Encrypt secrets in Git with SOPS and age

Keep encrypted secrets safely in your repo, decrypt them in CI, and never commit a plaintext credential again.

Mar 3, 2026·4 min readIntermediate·By the SecOpsLog team · command-tested

GitOps wants every config file in the repo. Secrets are config files too — and putting a database password in plaintext is how you turn a private repository into a slow-motion breach. SOPS (Secrets OPerationS) encrypts only the values in YAML or JSON, leaving keys readable so diffs stay reviewable. Pair it with age (a small modern encryption tool from Filippo Valsorda) and you get envelope encryption without a KMS bill, without a key server, and without pretending base64 is security.

This walkthrough generates an age keypair, wires .sops.yaml so every file under secrets/ encrypts to the same recipients, encrypts a sample file in place, and decrypts it in CI the way a GitOps controller would. If you are still deciding where secrets should live, start with Secrets management foundations — SOPS is the locked drawer, not the staffed key desk.

How SOPS + age wraps a secrets file

SOPS generates one data key per file, encrypts values with it, then wraps that data key for each age recipient. The sops: metadata block at the bottom is plaintext — that is by design.

1age-keygencreates X25519 keypair2.sops.yamlmaps path → recipients3sops -e -iencrypt values in place4git commitciphertext in history5CI / controllerSOPS_AGE_KEY in env6sops -dplaintext at deploy only7kubectl applynever stored in Git

Generate an age keypair and keep the private half out of Git

Each person or CI job that decrypts needs its own age private key. The public key goes in .sops.yaml; the private key lives in a password manager, CI variable, or cluster Secret — never in the repository. Rotating means generating a new key, re-encrypting files with both recipients briefly, then dropping the old recipient.

bash — generate and inspect keyslive
age-keygen -o ~/.config/sops/age/keys.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8...
chmod 600 keys.txt — treat it like a password
grep age1 ~/.config/sops/age/keys.txt
copy the public key into .sops.yaml only

One .sops.yaml for the whole repo

Creation rules match file paths to recipient lists. You can scope different folders to different teams — platform ops decrypts secrets/infra/, app teams decrypt secrets/apps/shop/ — without maintaining separate encryption scripts.

.sops.yaml
creation_rules:
- path_regex: secrets/.*\.yaml$
encrypted_regex: '^(data|stringData|password|token|secret)$'
age: >-
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8...,
age1another_teammate_public_key...

Encrypt in place and verify the diff

Run sops -e -i before the first commit of a secrets file. After encryption, keys like db_password stay readable in the diff; only the value becomes an ENC[...] blob. That is what makes encrypted secrets reviewable in pull requests.

bash — encrypt and inspectlive
sops -e -i secrets/prod/database.yaml
git diff secrets/prod/database.yaml
db_password: ENC[AES256_GCM,data:9f2a...,type:str]
key readable, value ciphertext — safe to commit
sops -d secrets/prod/database.yaml | yq .db_password
decrypt locally only when you need to edit a value
Encrypt before the first commit
Once plaintext lands in git history, rotation is the only real fix — deleting the file in the next commit does not erase clones, forks, or backups. Add a [gitleaks pre-commit hook](/blog/gitleaks-precommit/) so a fat-fingered paste never reaches the remote.

Decrypt in CI without writing secrets back to disk in Git

Export the age private key as SOPS_AGE_KEY from your CI secret store — GitLab masked variable, GitHub Actions secret, whatever you already trust. Decrypt to a temp file, apply, delete. For Kubernetes GitOps, Flux and Argo CD both have SOPS integrations that decrypt at reconcile time so the controller holds the key, not every developer laptop.

.gitlab-ci.yml
deploy:
variables:
SOPS_AGE_KEY: $AGE_PRIVATE_KEY # from CI secret store, never in repo
script:
- export SOPS_AGE_KEY
- sops -d secrets/prod/database.yaml > /tmp/db.yaml
- kubectl apply -f /tmp/db.yaml
- shred -u /tmp/db.yaml
age vs cloud KMS backends
age (this post)
No AWS/GCP bill
Keys you rotate yourself
Great for small teams + GitOps
Private key must be guarded
AWS KMS / GCP KMS
Central audit + IAM
Hardware-backed keys
Better at org scale
See [Advanced secrets](/courses/secrets-adv/)

Where this goes next

SOPS solves storage in Git. It does not tell you who read a secret, rotate it on a schedule, or issue short-lived database credentials. When you outgrow encrypted files, move the source of truth to Vault and sync into the cluster with External Secrets. The SOPS course covers KMS backends, multi-recipient rotation, and Flux/Argo integration end to end.

Go deeper in a courseSOPSEncrypt secrets in Git with SOPS, age, and cloud KMS backends.View course

Related posts