Skip to main content
Secure Coding Practices

Why Your Code Is Leaking Secrets (and How to Plug the Ocean)

Every week, another headline about a leaked database or compromised API key. Often the root cause isn't a sophisticated attack—it's a developer committing a secret to a public repo, or a log file that accidentally captured an environment variable. This article is for anyone who writes code that touches credentials, tokens, or keys: backend engineers, DevOps practitioners, and team leads. We'll show you where secrets leak, why common fixes backfire, and how to build a practical defense that doesn't slow you down. Where Secrets Actually Leak in Real Projects Secrets don't vanish into thin air—they escape through specific, predictable gaps. The most common is the version control system. A developer adds a .env file to a quick prototype, commits it, and pushes. Even if they remove it later, the secret lives forever in the git history. Another frequent leak is application logs.

Every week, another headline about a leaked database or compromised API key. Often the root cause isn't a sophisticated attack—it's a developer committing a secret to a public repo, or a log file that accidentally captured an environment variable. This article is for anyone who writes code that touches credentials, tokens, or keys: backend engineers, DevOps practitioners, and team leads. We'll show you where secrets leak, why common fixes backfire, and how to build a practical defense that doesn't slow you down.

Where Secrets Actually Leak in Real Projects

Secrets don't vanish into thin air—they escape through specific, predictable gaps. The most common is the version control system. A developer adds a .env file to a quick prototype, commits it, and pushes. Even if they remove it later, the secret lives forever in the git history. Another frequent leak is application logs. A well-intentioned debug statement prints the full request payload, including an authorization header. That log gets shipped to a centralized logging service with weak access controls, and suddenly the secret is visible to anyone on the team—or worse, to third-party log viewers.

CI/CD pipelines are another major vector. Build scripts often need secrets to install dependencies or deploy artifacts. If those secrets are passed as plain-text environment variables in the pipeline configuration, they can appear in build logs or be exposed through debug artifacts. We've seen cases where a CI job printed the entire environment to help debug a flaky test, inadvertently exposing production database credentials.

Microservice architectures introduce their own challenges. Service A needs to authenticate to Service B, so developers hard-code a shared secret in the configuration file. That file gets copied across environments, checked into repos, and eventually ends up on a developer's laptop that gets lost. The secret is now effectively public.

Finally, there's the human factor. Developers paste secrets into chat channels, share screenshots with credentials visible, or store them in unencrypted notes. These are hard to catch with automated tools, but they're just as dangerous. The first step to plugging leaks is knowing where to look—and these five areas cover the vast majority of incidents.

Version Control Leaks

Git history is permanent. Even after a force push, the commit may exist on a colleague's local clone or a CI cache. Tools like git-secrets and pre-commit hooks can scan for patterns before they reach the remote, but they're only effective if the team enforces them. A single bypass—like using --no-verify—can undo the protection.

Log Exposure

Logging frameworks often include sensitive data by default. For example, Java's Logback or Python's logging module can print request parameters if the log level is set to DEBUG. The fix is to implement a custom filter that redacts known secret patterns before the log line is written. But this requires maintaining a list of patterns, which can fall out of date.

Foundations Readers Confuse: Encryption vs. Secret Management

A common misconception is that encrypting secrets is the same as managing them. Encryption is a tool, not a strategy. You can encrypt a file containing secrets, but if the decryption key is stored next to the file, you've gained nothing. Secret management is about controlling access, rotation, and audit trails—not just scrambling bytes.

Another confusion is between environment variables and secret stores. Environment variables are convenient, but they're not secure. Any process running on the same machine can read them via /proc/self/environ or a debugger. Container orchestration platforms like Kubernetes store environment variables in plain text in the pod spec unless you use a Secret resource—and even then, the secret is base64-encoded by default, which is not encryption.

Teams also confuse 'secret rotation' with 'secret generation.' Rotation means regularly replacing a secret with a new one to limit the window of exposure. Generation is just creating a random string once. A secret that never rotates is a ticking bomb—if it leaks, it's valid forever. Many teams generate secrets correctly but never set up rotation, assuming it's handled by the platform.

Finally, there's the myth that 'we don't have secrets because we use OAuth.' OAuth tokens are secrets too. If a client ID and client secret are hard-coded, or if a refresh token is stored insecurely, you have the same problem. The protocol doesn't eliminate the need for secret management—it just changes the type of secret.

Environment Variables Are Not a Vault

Environment variables are passed to child processes, visible in /proc, and often logged during startup. They're fine for non-sensitive configuration like log levels, but not for production credentials. Use a vault that injects secrets into memory only when needed.

Base64 Is Not Encryption

Kubernetes Secrets are base64-encoded by default. Anyone with access to the cluster's etcd or the API can decode them. Enable encryption at rest for etcd and use a secrets store like HashiCorp Vault or AWS Secrets Manager for actual protection.

Patterns That Usually Work for Containing Secrets

After years of seeing what fails, the industry has converged on a few reliable patterns. The first is using a dedicated secret management service. Tools like HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, and GCP Secret Manager provide a centralized API for storing, retrieving, and rotating secrets. They also offer audit logs so you can see who accessed what and when.

The second pattern is dynamic secrets. Instead of a static API key that lives for months, the vault generates a temporary credential on demand. For example, a database user gets a username and password that expire after 15 minutes. If that credential leaks, it's useless by the time an attacker tries to use it. This dramatically reduces the blast radius.

The third pattern is injecting secrets at runtime, not build time. Build artifacts like Docker images should never contain secrets. Instead, the orchestration layer (Kubernetes, Nomad, or a cloud service) fetches the secret from the vault and injects it as a volume mount or environment variable at container start. This way, the secret never touches the build pipeline.

Another effective pattern is using a sidecar or init container to fetch secrets. The main application doesn't know how to talk to the vault—it just reads a file from a shared volume. The sidecar handles authentication and renewal. This keeps the application code simple and the secret management logic separate.

Finally, automated rotation with a schedule is critical. Even if you use static secrets, rotate them every 90 days or less. Many vaults support automatic rotation for common services like databases and cloud APIs. If a secret is compromised, you can rotate it immediately without manual intervention.

Dynamic Secrets in Practice

With HashiCorp Vault's database secrets engine, you can configure a lease duration. When a pod requests a database credential, Vault creates a new user with a random password and grants it limited permissions. The pod uses the credential for its lifetime, then Vault deletes the user. No long-lived secrets exist.

Sidecar Injection

In Kubernetes, you can use a mutating admission webhook to automatically inject a Vault agent sidecar into every pod. The sidecar fetches secrets and writes them to a shared emptyDir volume. The application reads from that volume. This pattern works for any language and requires zero code changes.

Anti-Patterns and Why Teams Revert to Them

Despite good intentions, teams often slip back into risky habits. One common anti-pattern is storing secrets in a private Git repository. 'It's private, so it's safe,' the reasoning goes. But private repos are accessible to many people—contractors, former employees, CI systems—and a single compromised account exposes all secrets. Git history also makes revocation nearly impossible.

Another anti-pattern is using a single master key for everything. A team might encrypt all secrets with one AES key and store that key in an environment variable. If that key leaks, every secret is compromised. This is a single point of failure that defeats the purpose of encryption.

Teams also revert to hard-coding secrets during development because it's faster. 'I'll fix it before production,' they say. But production deadlines hit, and the hard-coded secret stays. The fix is to make secret injection part of the development workflow from day one—use a local vault instance or a mock that mimics the production setup.

Another revert trigger is when the secret management tool is too complex. If setting up a vault takes weeks, developers will find workarounds. Choose a tool that matches your team's maturity. For small teams, a cloud-managed secret store with a simple SDK might be better than a full Vault cluster.

Finally, over-rotation is a real problem. If secrets rotate every hour, applications that cache credentials will break. The result is that operators disable rotation entirely. Find a rotation interval that balances security with operational stability—typically 24 hours to 90 days depending on the risk.

The 'Private Repo' Fallacy

GitHub's private repos are not a security boundary. Any collaborator with read access can clone the repo and extract secrets. Use a vault, not a repo, for secrets.

Complexity-Driven Reversion

When a secret management tool requires a dedicated team to maintain, developers will bypass it. Start simple: use the cloud provider's built-in secret manager before adopting a third-party tool.

Maintenance, Drift, and Long-Term Costs

Secret management isn't a one-time setup. Over time, secrets accumulate. Old API keys that are no longer used but still valid. Service accounts with excessive permissions. Rotation schedules that were never configured. This is secret drift—the gradual divergence between what's actually deployed and what's documented.

Drift happens because teams don't have a process for decommissioning secrets. When a microservice is retired, its database credentials should be revoked. But often they're left active, creating a potential backdoor. The fix is to tag secrets with metadata (owner, expiration date, service name) and run regular audits to identify orphaned secrets.

Another long-term cost is the operational burden of managing the vault itself. If you run your own Vault cluster, you need to handle backup, replication, unsealing, and upgrades. A misconfigured vault can become a single point of failure—if it's down, no new secrets can be issued, and applications may fail to start. Cloud-managed options reduce this burden but come with vendor lock-in.

Audit logs also require maintenance. Storing logs indefinitely is expensive, but deleting them too early means you can't investigate past breaches. Set a retention policy based on compliance requirements (e.g., 1 year for PCI-DSS, 7 years for HIPAA). Regularly review logs for anomalous access patterns.

Finally, training costs are ongoing. Every new developer needs to learn the secret management workflow. If the process is manual or poorly documented, they'll make mistakes. Invest in onboarding scripts and clear runbooks to reduce the learning curve.

Auditing for Orphaned Secrets

Run a quarterly scan that lists all secrets and checks if the associated service still exists. Revoke any secret that hasn't been accessed in 90 days. Automate this with a script that queries the vault API and compares against your service inventory.

Vault Cluster Maintenance

If you self-host Vault, set up a monitoring dashboard for seal status, request latency, and storage usage. Plan for upgrades during maintenance windows—Vault has breaking changes between major versions.

When Not to Use This Approach

Secret management tools aren't always the answer. For a personal project or a prototype that will never see production, using environment variables or a .env file is acceptable. The risk is low, and the overhead of setting up a vault is not justified.

Another case is when the application runs in a fully trusted environment with no external access. For example, an internal tool that runs on a locked-down server with no network access to the internet. In that scenario, file-based secrets with strict file permissions may be sufficient. However, 'trusted' environments have a way of becoming less trusted over time, so consider this a temporary measure.

If your team is very small (1-2 developers) and you're not subject to compliance requirements, a password manager shared among the team can work. Store secrets in the password manager and copy them to local .env files. This is not scalable but it's better than hard-coding.

Finally, avoid over-engineering for ephemeral environments like CI/CD pipelines that run for minutes. For short-lived build jobs, using environment variables injected by the CI platform is acceptable, as long as the platform itself has proper access controls. Just ensure the build logs are scrubbed and not stored long-term.

Local Development Pragmatism

For local development, use a .env file that is listed in .gitignore. Never commit it. If you need multiple configurations, use a tool like direnv that loads environment variables per directory. This is fast and simple, but don't let it leak into production.

Single-User Projects

If you're the only developer and the project is not critical, a simple encrypted file with a passphrase is fine. Use a tool like age or GPG to encrypt the file, and store the passphrase in a password manager.

Open Questions and Common Pitfalls

One question that comes up often: 'Should I encrypt secrets in the database?' The answer depends on the threat model. If the attacker has access to the database, they may also have access to the encryption key. Encryption at rest is useful for protecting against physical theft of disks, but it doesn't protect against a live database compromise. For that, you need application-level encryption with a key stored outside the database.

Another frequent question is about secret rotation for third-party APIs. Many external services don't support multiple active keys, so rotation requires a brief window of downtime. The workaround is to generate a new key, update the application, then revoke the old key. Some services allow two overlapping keys—use that feature when available.

Teams also ask about secret scanning in CI. Tools like GitLeaks and TruffleHog can scan code for secrets before merge. But they generate false positives (e.g., a commit hash that looks like a secret). Tuning the rules is essential to avoid alert fatigue. Start with a broad set of rules and gradually narrow them based on your team's feedback.

Finally, a common pitfall is not testing the secret rotation process. Many teams set up rotation but never simulate a failure. When the vault is down or the rotation script fails, they're caught off guard. Run a chaos engineering exercise where you rotate a critical secret and verify that the application recovers without manual intervention.

Handling Third-Party API Key Rotation

For APIs that don't support overlapping keys, schedule a maintenance window. Create the new key, deploy the updated application, then revoke the old key. Document the procedure and test it annually.

Reducing False Positives in Secret Scanning

Use a baseline file that lists known false positives (e.g., test tokens). Most secret scanners support a .gitleaks.toml or similar configuration. Review the baseline quarterly to ensure it doesn't hide real secrets.

Summary and Next Experiments

Leaking secrets is not inevitable. The core principles are: never store secrets in code or config files, use a vault for dynamic secrets, inject at runtime, rotate regularly, and audit access. Start with a cloud-managed secret store if you're new—it reduces operational overhead. For existing projects, run a scan to find hard-coded secrets and prioritize the ones with the highest blast radius (database credentials, cloud API keys).

Your next moves: (1) Set up a secret scanner in your CI pipeline and fix any findings. (2) Migrate one critical service to use a vault with dynamic secrets. (3) Implement a rotation policy for all static secrets. (4) Review audit logs for unusual access patterns. (5) Document your secret management workflow and include it in onboarding. These steps won't eliminate all risk, but they'll plug the biggest holes and give you a foundation to improve from.

Share this article:

Comments (0)

No comments yet. Be the first to comment!