How we migrated from .env files checked into repos to a proper secrets management workflow with HashiCorp Vault and CI/CD integration.
Every team starts with .env files. Most know they shouldn't stay there. Here's how we actually migrated to proper secrets management without breaking everything.
.env files in every repo (some committed to Git by accident)Before migrating anywhere, we cataloged every secret:
# Find .env files across all repos
find . -name ".env*" -not -path "*/node_modules/*" | while read f; do
echo "=== $f ==="
grep -c "=" "$f"
done
We found 247 unique secrets across 18 repos. 31 of them were duplicates with different values (a problem in itself).
We evaluated three options:
| Feature | AWS Secrets Manager | HashiCorp Vault | SOPS |
|---|---|---|---|
| Auto-rotation | Yes | Yes (with plugins) | No |
| Audit logging | CloudTrail | Built-in | Git history |
| Dynamic secrets | Limited | Yes | No |
| Cost | $0.40/secret/month | Self-hosted or HCP | Free |
| Complexity | Low | Medium-High | Low |
We chose Vault because we needed dynamic database credentials and cross-cloud support.
We migrated service by service, not all at once:
# Before: reading from .env
import os
db_password = os.environ["DB_PASSWORD"]
# After: reading from Vault with fallback
import hvac
import os
def get_secret(path: str, key: str) -> str:
"""Read from Vault, fall back to env var during migration."""
try:
client = hvac.Client(url=os.environ["VAULT_ADDR"])
secret = client.secrets.kv.v2.read_secret_version(path=path)
return secret["data"]["data"][key]
except Exception:
# Fallback during migration period
env_key = path.replace("/", "_").upper() + "_" + key.upper()
return os.environ[env_key]
For GitHub Actions, we use the official Vault action:
- name: Import Secrets
uses: hashicorp/vault-action@v2
with:
url: https://vault.internal.company.com
method: jwt
role: deploy-api
secrets: |
secret/data/api/production DB_PASSWORD | DB_PASSWORD ;
secret/data/api/production API_KEY | API_KEY ;
Key decision: We use JWT auth with GitHub's OIDC token, so no long-lived credentials in CI.
git-secrets or trufflehog in CI to catch leaksThe migration took us 6 weeks for 18 services. The hardest part wasn't the tooling—it was getting every developer to stop putting secrets in Slack.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
How to write postmortems that lead to real improvements, not just documentation theater. Includes a template and real examples.
How we went from 200 alerts per week (most ignored) to 15 actionable alerts with clear runbooks and useful dashboards.
Explore more articles in this category
A real cost audit uncovered idle load balancers, oversized RDS instances, and forgotten snapshots. Here's what we found and how we fixed each one.
A hands-on RDS restore drill guide for small cloud teams that thought backups were covered until a timed restore test exposed missing steps, DNS confusion, and stale credentials.
A real-world multi-cluster traffic routing guide for SaaS teams that have outgrown a single Kubernetes cluster and need safer rollout control without a service-mesh science project.