Every project starts the same way. You need an API key, so you drop it in a .env file, add .env to .gitignore, and move on. Totally fine for day one. The problem is that day one becomes month eighteen, the .env approach quietly becomes your entire secrets strategy, and now you've got credentials scattered across developer laptops, CI configs, and server environment variables with no idea who has what or when anything last changed. Nobody decided to run secrets this way. It just accumulated. Let me walk through what actually goes wrong and what to do instead.
The .env file's real problems
The .env file isn't evil, it's just not a strategy, and here's where it breaks down as you grow. It lives on disk in plaintext, so anyone with access to that machine can read every secret. It gets copied around, to laptops, to servers, pasted into Slack when someone's setting up, and every copy is another place a secret can leak. There's no access control, it's all-or-nothing, if you can read the file you have everything. There's no audit trail, no way to know who accessed a secret or when. And there's no rotation story, changing a secret means hunting down every copy and updating it by hand, which means in practice nobody rotates anything, ever.
None of that matters with one developer and three secrets. All of it matters with a team and fifty.
The mistake that dwarfs all the others: secrets in git
Before anything else, the single most common and most damaging secrets mistake is committing them to your repository. It happens constantly, a key hardcoded in a file, a .env accidentally committed, a config with a password pushed up. And here's the part people underestimate: once a secret is in git history, adding it to .gitignore later does nothing, because it's already in the history, permanently, and anyone who can see the repo, or ever cloned it, has it.
Git remembers everything. A secret committed once and "removed" in the next commit is still sitting right there in the history. So the rule is absolute: secrets never go in git, not in code, not in config, not "temporarily." Use a scanning tool that catches secrets before they're committed, because humans miss them and the cost of a miss is high. And if one does get committed, treat it as compromised and rotate it immediately, don't just delete the line, because deleting the line doesn't un-leak it.
What a real secrets setup looks like
The core idea of proper secrets management is simple: store secrets in a dedicated system built for it, not scattered in files, and have your applications fetch them at runtime rather than having them sitting around baked into configs.
A dedicated secrets manager gives you the things .env can't. Secrets stored encrypted, not plaintext on disk. Real access control, so you can say this service can read these specific secrets and nothing more. An audit trail, so you know who or what accessed what and when. And a rotation story, so changing a secret is a managed operation, not a scavenger hunt. Your app authenticates to the secrets manager and pulls what it needs when it starts or when it needs it, so the secret lives in memory at runtime rather than persisting in a file someone can grab.
Least privilege applies to secrets too
Don't give everything access to every secret. A service should be able to read only the specific secrets it actually needs. That way, if one service is compromised, the attacker gets that service's handful of secrets, not the master list of everything. This is the same principle that runs through all security, scope access narrowly so a single compromise has a small blast radius, applied to the credentials themselves.
Rotate, and make it painless enough that you actually do
A secret that never changes is a secret that only accumulates risk, and the longer it lives the more places it may have leaked to. Rotation, regularly changing secrets, limits the damage of a leak you don't even know about yet. The reason nobody rotates with .env files is that it's agonizing. The reason proper secrets management matters is that it makes rotation a managed, sometimes automatic operation, which means it actually happens. Short-lived credentials that rotate automatically are the gold standard, because a leaked one expires fast on its own.
Don't leak them at the other end either
One easy-to-miss trap: secrets ending up in logs. An app that logs its config, or an error that dumps environment variables, can write your secrets straight into log files in plaintext, undoing all the careful storage. Be deliberate about never logging secrets, and treat logs as something that could leak, because they often have weaker access controls than you'd want for something holding credentials.
The mental model
Secrets are credentials, treat them like the keys they are. Never in git, ever, and rotate anything that slips in. Store them in a dedicated system, encrypted, with access control and an audit trail, not in plaintext files copied around. Give each thing access to only the secrets it needs. Rotate regularly, and use short-lived credentials where you can. And make sure they don't leak out through logs at the far end. The .env file was fine as a starting point. It was never supposed to be the whole plan, and the day it quietly became the plan is the day you took on risk you didn't decide to.
Top comments (0)