Encrypt Django Secrets in Git With SOPS
Keep Django environment secrets encrypted in the repository with Mozilla SOPS and age: encrypt .env and YAML files, decrypt during deploys with Ansible or CI, use cloud KMS keys for teams, and keep keys out of Git.
Problem statement
Secrets for a Django deployment (SECRET_KEY, database password, API keys) often live in a password manager, someone's laptop, or a CI variable screen, and drift from what the code expects. Committing them in plain text is out of the question. SOPS encrypts the values in .env, YAML, or JSON files while leaving keys readable, so encrypted secret files can live in Git next to the code: changes are reviewed in pull requests, history is kept, and only holders of the decryption key (a developer's age key, a CI key, or a cloud KMS key) can read them.
- Install
sopsandage; generate an age key per person and one for CI (age-keygen -o key.txt). - Add
.sops.yamlwithcreation_ruleslisting recipients (age public keys or KMS ARNs) per path. - Encrypt:
sops --encrypt --in-place deploy/secrets/prod.env; edit later withsops deploy/secrets/prod.env. - Decrypt at deploy time in CI or Ansible, writing a
chmod 600env file on the server (or a Kubernetes Secret). - Keep private keys out of the repository; store the CI key as a CI secret or use cloud KMS with IAM.
- Rotate recipients with
sops updatekeyswhen people leave.
Step-by-step solution
1. Keys and rules
age-keygen -o ~/.config/sops/age/keys.txt # prints the public key: age1...
# .sops.yaml
creation_rules:
- path_regex: deploy/secrets/prod\..*
age: >-
age1alice...,
age1bob...,
age1ciprod...
- path_regex: deploy/secrets/staging\..*
age: age1alice...,age1bob...,age1cistaging...
Each file is encrypted with a data key that is itself encrypted for every recipient, so any listed key can decrypt. For teams on AWS or GCP, add kms: or gcp_kms: entries so access is controlled with IAM instead of distributing age keys.
2. Encrypt the environment file
cat > deploy/secrets/prod.env <<'ENV'
SECRET_KEY=replace-with-generated-value
DATABASE_URL=postgres://app:password@db.internal:5432/app?sslmode=require
STRIPE_API_KEY=sk_live_...
ENV
sops --encrypt --in-place deploy/secrets/prod.env
git add deploy/secrets/prod.env .sops.yaml
The committed file looks like SECRET_KEY=ENC[AES256_GCM,data:...,type:str] plus a sops_ metadata block. Variable names stay visible, which makes diffs reviewable: a pull request shows which secret changed, not its value.
3. Decrypt during deploys
In GitHub Actions:
- name: Decrypt production secrets
env:
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY_PROD }}
run: sops --decrypt deploy/secrets/prod.env > prod.env
- name: Upload env file
run: |
scp prod.env deploy@server:/etc/myapp/env.new
ssh deploy@server 'install -m 600 /etc/myapp/env.new /etc/myapp/env && rm /etc/myapp/env.new'
With Ansible, the community.sops collection decrypts files as variables or templates them onto hosts. With Kubernetes and GitOps, tools such as the Flux SOPS integration or helm-secrets decrypt in-cluster into Kubernetes Secrets.
4. Edit and review
sops deploy/secrets/prod.env decrypts into your editor and re-encrypts on save. In code review, look for unexpected secrets being added or removed; the values remain hidden.
5. Rotate access
When a developer leaves, remove their key from .sops.yaml and run sops updatekeys on each file, then rotate the actual secret values they could read. Removing a recipient does not change values they have already seen.
Verification check: git grep -n "sk_live\|postgres://" -- deploy/secrets returns nothing readable; sops --decrypt deploy/secrets/prod.env works only with an authorised key; the server's /etc/myapp/env is mode 600 and the app passes check --deploy.
Explanation
SOPS uses envelope encryption: it generates a data key per file, encrypts each value with AES-GCM, and encrypts the data key separately for each recipient using age, PGP, or a cloud KMS. It also computes a MAC over all values to detect tampering. Because only values are encrypted, files stay structured and diff-friendly, which brings secrets into the same review and versioning workflow as code without exposing them. Access control moves to whoever holds a recipient key or KMS permission.
Edge cases / notes
- Plaintext leaks. Decrypted files must never be committed; add patterns like
*.decryptedandprod.env(outsidedeploy/secrets) to.gitignore. - Merge conflicts in encrypted files are hard to resolve by hand; decrypt both versions with
sopsand re-encrypt. - KMS outages block decryption; keep an age break-glass key stored securely offline.
- Not a runtime store. SOPS protects secrets at rest in Git; the deployed env file still needs strict permissions.
Next steps
- Use it in configuration management with Deploy Django With Ansible.
- See the full range of approaches in Manage Django Secrets in Production.
FAQ
Is it safe to commit SOPS-encrypted secrets?
Yes, if the private keys stay out of the repository and recipients are limited to people and systems that need access.
age or PGP for SOPS?
age is simpler and recommended for new setups. Cloud KMS is best for teams already managing IAM.
How does CI decrypt SOPS files?
Give CI its own age key as a CI secret, or grant its cloud identity KMS decrypt permission.
What happens when someone leaves the team?
Remove their key, run sops updatekeys, and rotate the secret values they had access to.