Yes, I commit secrets to git (with SOPS)
Encrypted secrets in version control that actually work. Share API keys across your team without Vault or secret managers.
Secrets don’t belong in git. Except when they do
I’ve seen people send me production passwords in plaintext over Teams. .env files in Slack messages that disappear after 90 days. Credentials shared in Google Docs
Now I keep production API keys, database passwords, and dashboard credentials in my repositories. Encrypted with SOPS, committed alongside code, accessible to everyone who needs them
No secret managers, no Vault clusters, no separate infrastructure. Just git

Why secrets in git works
Most teams solve secret sharing wrong. HashiCorp Vault needs its own infrastructure. AWS Secrets Manager costs money and locks you to AWS. Encrypted env files in Slack get lost
SOPS encrypts files with age or GPG. Each developer has a key. The encrypted file goes in git. Everyone with a key can decrypt it. Remove someone’s key when they leave
Simple, auditable, works offline
Setup takes 5 minutes
Install SOPS and age:
# Debian/Ubuntu
apt install age
go install github.com/getsops/sops/v3/cmd/sops@latest
# Arch
yay -S sops age Generate a key for yourself:
age-keygen -o ~/.config/sops/age/keys.txt This outputs your public key. Share it with your team. Keep the private key secret
Team onboarding
New developer joins. They generate their age key and send you the public key. You add it to .sops.yaml:
creation_rules:
- path_regex: secrets/.*\.yaml$
age: >-
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
age1qlmn8m9x7xj8p6vfz2k5h3r8w6t4y9q2v5x8c3n7b4m6a1d9e2f5g8h1j4k
encrypted_regex: '^(data|stringData|password|api_key|secret)$' Each line is one person’s public key. Add the new dev’s key, commit the change
Re-encrypt all secrets to include the new key:
sops updatekeys secrets/**/*.yaml
git add secrets/
git commit -m 'add key for new developer' Now they can decrypt everything. Takes 2 minutes
Creating secrets
I built a task runner command for this. task sops:create secrets/api-keys.yaml opens vim with an encrypted file:
# Makefile or Taskfile.yml
sops:create:
cmds:
- |
if [ ! -f {{.CLI_ARGS}} ]; then
echo 'Creating new encrypted file: {{.CLI_ARGS}}'
sops {{.CLI_ARGS}}
else
echo 'File exists, editing: {{.CLI_ARGS}}'
sops {{.CLI_ARGS}}
fi Edit the file in your editor. SOPS handles encryption on save. Commit it:
task sops:create secrets/prod/database.yaml
# Edit in vim, add credentials
git add secrets/prod/database.yaml
git commit -m 'add production database credentials' The committed file is encrypted. Only team members with keys can read it
Using secrets in CI/CD
GitHub Actions needs the age private key. Add it as a repository secret named SOPS_AGE_KEY. Then decrypt in your workflow:
- name: Decrypt secrets
env:
SOPS_AGE_KEY: \$\{\{ secrets.SOPS_AGE_KEY \}\}
run: |
sops -d secrets/prod/api-keys.yaml > api-keys.yaml
export API_KEY=$(yq '.api_key' api-keys.yaml) Weak point: Your CI holds the private key. This is the most sensitive part of the system. If you use a poorly isolated self-hosted runner or someone can execute arbitrary code in your CI, they can extract SOPS_AGE_KEY and decrypt everything
We locked this down with surgical precision:
- Only admins can modify workflows (branch protection on
.github/workflows/) - GitHub secrets are only accessible to repo admins
- External PRs don’t trigger workflows automatically (prevent pwn requests)
- GitHub-hosted runners only, never self-hosted for production
- Branch protection: no direct push to main, even for admins
Result: to steal SOPS_AGE_KEY, you need to be a repo admin AND successfully merge a malicious workflow. With CODEOWNERS and mandatory review, it’s nearly impossible
Same works for GitLab CI, CircleCI, any CI system. Set the env var, decrypt the file
For Kubernetes, decrypt and apply:
sops -d secrets/prod/k8s-secrets.yaml | kubectl apply -f - Approval workflow for access
Use branch protection and CODEOWNERS. Only senior devs can approve changes to .sops.yaml:
# CODEOWNERS
.sops.yaml @senior-team
secrets/** @senior-team New dev wants access? They open a PR adding their public key. Senior dev reviews, approves, merges. The re-encryption happens automatically in CI:
# .github/workflows/reencrypt.yml
name: Re-encrypt secrets
on:
push:
paths:
- '.sops.yaml'
jobs:
reencrypt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install SOPS
run: |
wget https://github.com/getsops/sops/releases/latest/download/sops-linux-amd64
sudo mv sops-linux-amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
- name: Re-encrypt all secrets
env:
SOPS_AGE_KEY: \$\{\{ secrets.SOPS_AGE_KEY \}\}
run: |
find secrets -name '*.yaml' -exec sops updatekeys {} \;
- name: Commit changes
run: |
git config user.name 'GitHub Actions'
git config user.email 'actions@github.com'
git add secrets/
git diff --quiet && git diff --staged --quiet || git commit -m 're-encrypt secrets with new recipients'
git push Every time .sops.yaml changes, CI re-encrypts everything. New person gets access instantly. Old person loses access when you remove their key
Developer leaves the team
Remove their public key from .sops.yaml. Commit and push. CI re-encrypts everything without their key. They can’t decrypt new secrets
Warning: Removing their key only prevents them from reading new secrets. They still have the entire git history. They still have access to every secret they decrypted before leaving. That’s why you need to rotate actual secrets:
# Update the actual passwords/keys in your services
# Then update the encrypted files
sops secrets/prod/database.yaml
# Change the password values
git commit -m 'rotate production credentials' This is the only manual step. Everything else is automatic
File structure that scales
secrets/
dev/
api-keys.yaml
database.yaml
staging/
api-keys.yaml
database.yaml
prod/
api-keys.yaml
database.yaml
terraform.yaml
.sops.yaml Different keys for different environments. Devs get dev and staging. Ops get everything:
creation_rules:
- path_regex: secrets/dev/.*\.yaml$
age: age1dev1...,age1dev2...,age1ops1...
- path_regex: secrets/staging/.*\.yaml$
age: age1dev1...,age1dev2...,age1ops1...
- path_regex: secrets/prod/.*\.yaml$
age: age1ops1...,age1ops2... Fine-grained access control without infrastructure
Comparing to alternatives
Vault: Requires running infrastructure. SOPS needs zero infrastructure. Files are just git
AWS Secrets Manager: Cloud vendor lock-in. Costs money. SOPS works anywhere
Encrypted .env in Slack: Gets lost. No version history. SOPS has full git history and audit trail
1Password/Bitwarden for teams: Separate system. SOPS lives with code. Deploy uses same git SHA for code and secrets
SOPS wins when you want secrets versioned with code
Common issues
Lost private key? You’re locked out. Back up your ~/.config/sops/age/keys.txt. Store it in your password manager. No backup means no recovery. All your secrets become permanently inaccessible
Merge conflicts in encrypted files? Decrypt both versions, manually merge, re-encrypt:
sops -d secrets/file.yaml > file-yours.yaml
git checkout main
sops -d secrets/file.yaml > file-theirs.yaml
# Manually merge file-yours.yaml and file-theirs.yaml
sops secrets/file.yaml # Edit with merged content Someone committed a plaintext secret? Use BFG Repo-Cleaner to remove it from git history. Rotate the secret immediately. But know that if the repo is public or someone already pulled, it’s too late. The secret is burned. GitHub scans public commits for secrets, you’ll get an alert within minutes
Real workflow
New developer needs access. I tell them to run one command:
task sops:onboarding That’s it. The command installs dependencies, generates their age key, and sends me their public key automatically. I approve the PR, merge, CI re-encrypts everything. They pull and have access to all secrets
Under the hood, the task does this:
sops:onboarding:
cmds:
- |
# Detect package manager and install
if command -v yay &> /dev/null; then
yay -S --noconfirm sops age
elif command -v apt &> /dev/null; then
apt update && apt install -y age
go install github.com/getsops/sops/v3/cmd/sops@latest
fi
- mkdir -p ~/.config/sops/age
- age-keygen -o ~/.config/sops/age/keys.txt
- |
PUBLIC_KEY=$(grep 'public key:' ~/.config/sops/age/keys.txt | awk '{print $4}')
echo 'Your public key: $PUBLIC_KEY'
echo 'Send this to your team lead to get access to secrets' They could do it manually. Install SOPS and age, generate a key, grep for the public key, send it. But why make them memorize commands when you can automate it?
Same for me adding their key. I have task sops:add-key PUBLIC_KEY=age1... that updates .sops.yaml, commits, pushes. Takes 5 seconds
Create a new secret:
task sops:create secrets/staging/new-api.yaml
# Edit file, add credentials
git add secrets/staging/new-api.yaml
git commit -m 'add staging API credentials'
git push Use it in deployment:
sops -d secrets/staging/new-api.yaml | kubectl apply -f - No separate systems. No UI to click. Everything is code and git
I used to avoid putting secrets in git. Now I can’t imagine managing them any other way. Version controlled, auditable, accessible, encrypted. SOPS made secret management boring again, which is exactly what it should be
Reality is often more nuanced. But me? Nuance bores me. I'd rather be clear.