Look ma, no secrets manager

There’s a moment in every serious project where your deploy gets blocked on one question: “Where do the secrets live?”
This post covers the most straightforward bootstrap approach I’ve found: encrypted secrets directly in git. No servers. No cloud key service. No chicken-and-egg problem. Just quick, practical secrets encryption.
I encrypted it for you. You’re welcome.
I first learned about SOPS years ago at a conference talk where a presenter described their “secret handling process”:
“When I see a secret in source control, I encrypt it with SOPS, tell everyone how to decrypt it, and then tell them the password is in the team password manager, and they’re welcome to improve the process now that it’s encrypted.” - Anonymous security engineer
OK, look. It’s mildly passive-aggressive. But it makes the point: the real offense wasn’t git. It was checking in secrets unencrypted. “Encrypted + I sent you the password” isn’t perfect, but it’s a meaningful upgrade. Here’s the cleaner, repeatable version: SOPS + age.
The core idea
- SOPS is a file editor that transparently encrypts/decrypts YAML/JSON/ENV values.12
- SOPS uses the age encryption project to encrypt each file’s data key to public keys called “recipients.”3
- Anyone holding a matching private key can decrypt the file; everyone else just sees ciphertext.
Used together, you get encrypted secrets in git without running a secrets manager. The files live next to your code, and only the keys you’ve approved can decrypt them.
It’s essentially Ansible Vault’s model — encrypted data committed to git — generalized to everything.
Plain secrets in git have a nasty property: even if you delete them, they’re still in history. Every git clone ships the full liability, even if you later delete them. SOPS sidesteps this — old commits contain old ciphertext, not old secrets.
Why this is useful
This shines when you need something workable now, not after a multi-quarter rollout.
It’s also great for bootstrapping OpenBao itself. You need secrets up front (TLS, root tokens, service credentials). I use SOPS + age for bootstrap secrets, then move runtime secrets into OpenBao once it’s stable.45
This won’t save you from a hacked laptop/workstation. A secrets manager can be phished too. But it gives you a strong default: encrypted secrets in git, with decryption that requires the private key, on day one.
There’s a newer reason this matters: LLM-assisted dev has changed the stakes. Plaintext secrets in a repo don’t stay local. If you’re using Cursor, Copilot, or similar tools, your editor is routinely sending file contents to external services.
How this works in practice
SOPS works with anything that reads files: Ansible, Terraform, Kubernetes, shell scripts, whatever. The encrypted files are still just files.
Here’s what it looks like in an Ansible repo:
inventory/
host_vars/
web-01/
secrets.sops.yaml
.sops.yaml
The community.sops vars plugin decrypts these files when you run Ansible.6 Install it with ansible-galaxy collection install community.sops.
vars_plugins_enabled = host_group_vars,community.sops.sops
The important part: .sops.yaml rules
Generate age keys for each person or system that needs access:
age-keygen -o ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt
Create .sops.yaml in your repo root and add the recipients:
creation_rules:
- path_regex: inventory/(host_vars|group_vars)/.*/secrets\.sops\.ya?ml$
age:
- age1qyq3p7x...
- age1m8k5n2j...
- age1h4j7k0l...
SOPS encrypts values but leaves keys readable.
Workflow
To encrypt a plaintext file for the first time:
sops -e -i inventory/host_vars/web-01/secrets.yaml
mv inventory/host_vars/web-01/secrets.yaml inventory/host_vars/web-01/secrets.sops.yaml
To edit encrypted secrets:
sops inventory/host_vars/web-01/secrets.sops.yaml
It opens decrypted in your editor and re-encrypts on save.
To rotate recipients later:
sops updatekeys -y inventory/host_vars/web-01/secrets.sops.yaml
This re-encrypts the file for a new set of recipients; it doesn’t change the secret values.
Important: Add a .gitignore rule so only *.sops.yaml secrets files are allowed:
# Don't allow unencrypted secrets
inventory/**/secrets.yaml
inventory/**/secrets.yml
# Encrypted secrets are OK
!inventory/**/secrets.sops.yaml
!inventory/**/secrets.sops.yml
For CI/CD, add a separate age recipient for your CI machine. Same files, automated decryption. Later, you can switch to cloud key management (AWS, GCP, Azure) or OpenBao Transit for automatic rotation.
Hardware-backed keys with YubiKey
If you want the private key protected by hardware instead of a file on disk, age supports YubiKeys via the PIV smart-card interface.7
This is strictly optional — everything in this post works without it.
Set SOPS_AGE_KEY_CMD="age-plugin-yubikey --identity" to tell SOPS to delegate cryptographic operations to the YubiKey instead of using a key file.
Notes:
- Not all YubiKey models support PIV.
- Initial setup is slightly more involved.
- You’ll need physical access to decrypt.
YubiKeys aren’t required, but they add a strong hardware boundary for private keys. My preferred setup uses three YubiKeys (because humans lose things), with PIN and touch required, and then use file based keys where required just for automation.
The tradeoffs
Here’s how different approaches stack up:
✅ = yes · ⚠️ = depends / partial · ❌ = no
| Approach | 🔒Encrypt | 📌Source | 👤Access | 🧾Audit | 🔁Rotate |
|---|---|---|---|---|---|
| Git (plain) | ❌ | ✅ | ❌ | ⚠️ | ❌ |
| SOPS | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ |
| OpenBao | ✅ | ✅ | ✅ | ✅ | ✅ |
| Cloud | ✅ | ✅ | ✅ | ✅ | ✅ |
Why SOPS gets partial marks:
- Access: Possession of the private key is the policy; revocation is manual (re-encrypt + redeploy).
- Audit: Git shows who changed secrets, not who decrypted/used them.
- Rotate: Re-encrypting for new recipients is manual, not policy-driven automation.
Which is to say: no, file-based encryption doesn’t replace a real secrets manager. But I do think it replaces the worst part of many secrets-manager rollouts: the initial phase where you need secrets before you have secrets infrastructure.
For many projects, that tradeoff is worth it: significantly better security for very little complexity.
“SOPS: Secrets OPerationS.” getsops/sops (GitHub), https://github.com/getsops/sops ↩︎
“Security.” getsops.io, https://getsops.io/ ↩︎
“age.md.” C2SP, https://github.com/C2SP/C2SP/blob/main/age.md ↩︎
“OpenBao.” openbao.org, https://openbao.org/ ↩︎
“Transit Secrets Engine.” OpenBao Documentation, https://openbao.org/docs/secrets/transit/ ↩︎
“Protecting Ansible secrets with SOPS.” Ansible community.sops documentation, https://docs.ansible.com/projects/ansible/latest/collections/community/sops/docsite/guide.html ↩︎
“age-plugin-yubikey.” str4d/age-plugin-yubikey (GitHub), https://github.com/str4d/age-plugin-yubikey ↩︎