I found my Gmail password in Git. In a public repo. Three months after I pushed it.
Not a commit from years ago that I’d forgotten about. A commit from last Tuesday. In apps/monitoring/values.yaml:
alertmanager:
config:
global:
smtp_auth_password: 'your-app-password' # right there in plaintext
The commit was public. The repo was public. The password was public. And I had no idea until I was browsing GitHub on my phone and saw it.
“Secrets in Git are not a matter of if they leak - they’re a matter of when.” - Every security audit ever
“The password is… oh.” - Every developer who found their Gmail token in a public repo. Time to fix this properly.
The Options
I looked at three approaches:
Sealed Secrets - Encrypt secrets, commit them to Git. Still didn’t like having encrypted blobs in my repo. What if there’s a crypto vulnerability someday? What if I misconfigure something?
SOPS - Similar idea, different tool. Better key management, but same fundamental issue: encrypted secrets in Git.
Vault - Secrets live in Vault. External Secrets Operator syncs them to K8s. Git has zero secrets.
I went with Vault. Also added Consul for configuration management (PUID, timezone, ports - stuff that isn’t secret but also shouldn’t require Git commits to change).
“Secrets and config are different problems. Secrets: who can access. Config: what values to use. Don’t conflate them.” - Secrets management principle
Is it overkill for a homelab? Absolutely. But I was trying to learn production patterns, and this is what production does.
How It Works
Vault stores secrets (passwords, API keys, tokens). External Secrets Operator authenticates to Vault using Kubernetes service accounts, pulls secrets, creates K8s Secrets. Apps consume those Secrets like normal. They don’t know Vault exists.
Consul stores configuration (ports, resource limits, timezone, etc.). Init containers fetch config on startup and write it to files. Apps read those files. Again, apps don’t know Consul exists.
Backups run daily. Export Vault secrets and Consul config to NFS. Keep 30 days of history.
The apps themselves don’t change. They still read K8s Secrets and config files. We just changed where those come from.
Deploying Vault
I used dev mode. Don’t do this in production - it runs in-memory with auto-unsealing and a hardcoded root token - but for homelab it’s perfect. No persistent storage setup, no unsealing process, just works.
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault -n vault --create-namespace --set server.dev.enabled=true
Root token is root. Data is in-memory. Pod restart = data loss. That’s fine - I set up automated backups later.
Setting Up Vault
Vault needs some config: enable KV secrets engine, set up Kubernetes authentication, create policies. I automated this with a Job that runs after Vault starts:
vault login root
vault secrets enable -path=secret kv-v2
vault auth enable kubernetes
# ... policy and role creation
External Secrets Operator
This is the bridge between Vault and Kubernetes. It authenticates to Vault, pulls secrets, creates K8s Secrets automatically.
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace
Configure a SecretStore to tell it how to reach Vault:
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: vault-backend
namespace: media
spec:
provider:
vault:
server: "http://vault.vault.svc:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
role: "external-secrets"
Moving Secrets to Vault
I exec’d into the Vault pod and started populating secrets:
kubectl exec -n vault vault-0 -it -- sh
vault login root
vault kv put secret/monitoring/smtp username='my-email@gmail.com' password='...'
vault kv put secret/monitoring/pushbullet token='...'
vault kv put secret/monitoring/grafana password='...'
Three secrets moved from Git to Vault. Repository is now clean.
Syncing to Kubernetes
Create ExternalSecret resources. ESO watches Vault and creates K8s Secrets automatically:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: smtp-credentials
namespace: media
spec:
refreshInterval: "1h"
secretStoreRef: {name: vault-backend}
target: {name: smtp-credentials}
data:
- secretKey: smtp_password
remoteRef: {key: monitoring/smtp, property: password}
kubectl apply -f externalsecrets.yaml
kubectl get externalsecrets -n media # Should show SecretSynced
ESO creates a K8s Secret called smtp-credentials. Apps consume it like any other Secret. They don’t know it came from Vault.
Updating Apps
The apps didn’t change. Before, they read secrets from hardcoded Helm values. Now they read the same secrets from K8s Secrets (which ESO populates from Vault).
Changed apps/monitoring/values.yaml from:
smtp_auth_password: 'your-app-password' # ❌ hardcoded
To:
# SMTP password in Vault: secret/monitoring/smtp
env:
SMTP_PASSWORD:
secretKeyRef: {name: smtp-credentials, key: smtp_password}
Redeployed. Worked. No app changes needed.
Rotating Secrets
Now when I need to rotate a password:
kubectl exec -n vault vault-0 -- vault kv put secret/monitoring/smtp password='NEW_PASSWORD'
ESO syncs it within an hour. Or I delete the K8s Secret to force immediate sync. No Git commit. No redeployment. Just update Vault, restart pods.
Consul for Configuration
Secrets were solved. But I had other problems: configuration values hardcoded everywhere. PUID, timezone, ports, resource limits - all in Git.
Deployed Consul, populated it with 37 config values:
consul kv put config/media/common/puid "1000"
consul kv put config/media/common/timezone "America/New_York"
consul kv put config/media/apps/sonarr/port "8989"
# ... 34 more
Apps fetch config via init containers on startup. Change timezone for all apps:
consul kv put config/media/common/timezone "Europe/London"
kubectl rollout restart -n media deployment/sonarr # picks up new value
No Git commit needed. Same pattern as Vault: centralize, fetch at runtime, never hardcode.
Automated Backups
Vault runs in dev mode (in-memory). Pod restart = data loss. So backups are critical.
CronJob runs daily at 2AM:
vault kv export -format=json secret/ > backup.json
gzip backup.json
# Store on NFS, keep 30 days
Same for Consul:
consul kv export config/ > backup.json
consul snapshot save snapshot.snap
# Compress, store, 30-day retention
I’ve tested restore. It works. That’s all that matters.
What I Learned
Dev mode is fine for homelab. Zero time spent on storage, unsealing, HA. It just works. Yes, data loss on pod restart. Backups run daily. I tested restore. The alternative is unsealing rituals and persistent volumes and “why isn’t Vault starting?” at 11pm. Dev mode. Accept it.
ESO is invisible to apps. They consume K8s Secrets. They don’t know Vault exists. Don’t care. No code changes. I migrated three apps. Changed nothing except removing hardcoded secrets. ESO created the Secrets. Apps kept working. It’s almost too easy. Almost.
Rotation is trivial. Update a value in Vault. Wait an hour. Or force sync. Done. No Git commits. No deployments. No “did I push that?” I rotated my SMTP password. Changed it in Vault. ESO propagated. Alertmanager picked it up. I did nothing else. Black magic. Good black magic.
Test your backups. I assumed they worked. They didn’t. The backup script had a bug - it was backing up an empty directory. I discovered this when I needed to restore. I no longer assume. I restore quarterly. Paranoid? Maybe. Prepared? Definitely.
Document the paths. Three months later I won’t remember if it’s secret/monitoring/smtp or secret/smtp/monitoring. I had to grep my own scripts to find a path. Comment everything. Your memory is not as good as you think.
The Result
Before: 3 secrets hardcoded in Git. Public repo. Anyone could see them.
After: Zero secrets in Git. All in Vault. Synced to K8s automatically. Rotatable without commits. Backed up daily.
Configuration moved from Git to Consul. Change timezone across 9 apps with one command, no deployment.
Repository is public now. No secrets, no embarrassment. Everything’s properly managed.
Worth It?
For homelab? Probably overkill. For learning production patterns? Absolutely.
The setup took a weekend. Vault + ESO + Consul + backups. Now:
- Rotate secrets without Git
- Change config without deployments
- Sleep better knowing nothing sensitive is public
- Actually learn how production does this
Would I do it again? Yes. Would I recommend it for everyone? No. If you’re just running Plex for yourself, hardcoded values in a private repo are fine.
But if you’re learning, if you care about security, or if your repo might ever be public - do this properly from the start.
Related
- Ansible Vault - Encrypt secrets in Ansible; different use case, same principle
- Velero Backups - Backup Vault data and Consul config
Full implementation: k8s-media-stack (see foundation/vault/, foundation/consul/, secrets/, backup/)