Search
Close this search box.
Search
Close this search box.
Secrets Management: How to Stop Leaking Your API Keys Online

Secrets Management: How to Stop Leaking Your API Keys Online

Then I watched a senior engineer rotate keys at 2am because one got pushed to a public repo for like… seven minutes. Seven. And by the time GitHub finished indexing it, bots had already found it, tested it, and started spending money.

So yeah. This happens to everyone. It’s not about being careless, it’s about how normal our workflows are.

You have a local .env file. You copy paste something into a config. You test a webhook. You share a screenshot. You open source a “quick demo”. You forget that one file is tracked. Or you assume private repos are private enough. Or you think a key that is “dev only” can’t hurt you.

And then it does.

This post is basically a practical guide to stop that from happening. Not theory. Not “just be careful”. Real systems and habits that make leaking secrets harder than breathing.

What counts as a “secret” (it’s more than API keys)

When people say secrets, they usually mean API keys. But the blast radius is bigger than that.

Here are the usual culprits:

  • API keys (Stripe, OpenAI, Twilio, SendGrid, Mapbox, AWS access keys, etc)
  • OAuth client secrets
  • Database connection strings (especially with embedded usernames and passwords)
  • Private keys (SSH keys, service account JSON files, JWT signing keys)
  • Webhook signing secrets
  • Session secrets, encryption keys, salts
  • Internal tokens (CI tokens, deploy tokens, personal access tokens)
  • .p12 files, .pem files, anything that looks like “I swear it’s fine”

If it authenticates. If it signs. If it can be used to access data or spend money. It’s a secret.

And if it’s in a place where it can be copied. It will be.

How leaks actually happen (the boring reality)

Most leaks are not dramatic. They’re boring. Which is why they keep happening.

1. Hardcoding in source code

The classic:

js const STRIPE_SECRET_KEY = “sk_live_…”;

Maybe it started as sk_test_... and “we’ll fix it later”. Later arrives, and someone swaps it, and forgets to move it out.

2. Committing .env or config files

Someone creates .env, runs the app, it works. Then they do git add . on autopilot.

Also, lots of frameworks generate files like:

  • .env.local
  • config.json
  • appsettings.Production.json
  • service-account.json

If it exists locally, assume someone will commit it at some point unless you design against it.

3. Copy pasting secrets into tickets, Slack, Notion, screenshots

This is more common than people admit.

  • A screenshot of the dashboard that includes a key.
  • A debug log pasted into a Jira ticket, includes Authorization header.
  • A “here’s how to reproduce” message, includes full curl command with token.

The thing is, those places get exported, indexed, backed up, shared. Forever.

4. CI logs and build output

CI is a secret shredder if you aren’t careful.

  • Echoing environment variables
  • Printing request headers in verbose mode
  • Stack traces that include DSNs or URLs with credentials
  • Misconfigured masking, so secrets appear in logs

And logs are often retained for months.

5. Client side exposure

If you ship a secret to the browser or a mobile app, it’s not a secret. Period.

  • Putting a private API key in React .env and building it
  • Embedding tokens in Android strings.xml
  • Shipping Firebase admin keys in a frontend bundle

If it’s in a client app, assume it’s public.

6. “Private repo” false safety

Private repos reduce risk, sure. But they do not eliminate it.

  • Contractors
  • Former employees with access
  • Dependency scanners
  • Backups
  • Accidental changes to visibility
  • Mirrored forks
  • Compromised accounts

Private does not mean safe. It means “fewer eyes”. Sometimes the wrong eyes still see it.

The mindset shift: treat secrets like radioactive material

You want two things:

  1. Secrets should not live in code.
  2. Secrets should not be handled manually more than necessary.

Because manual processes are basically just “human memory with worse uptime”.

Once you accept that, the rest becomes clearer. Put secrets in dedicated systems, inject them at runtime, rotate them, and detect leaks early.

Now let’s get practical.

Rule #1: Never store secrets in Git. Not even “temporarily”

This sounds obvious, but here’s the part people miss.

Even if you delete the secret in the next commit, it still exists in Git history.

Anyone who can access the repo can retrieve old commits. Also, GitHub and others may have already cached it, indexed it, or scanned it.

So, what do you do instead?

Use environment variables (but do it correctly)

Local dev:

  • Use .env files locally.
  • Load them with dotenv or your framework’s built in support.
  • Never commit the real .env.

In Git:

  • Commit a .env.example file, with dummy values and instructions.
  • Add .env and any real secret file patterns to .gitignore.

Example .env.example:

bash OPENAI_API_KEY=”your_key_here” STRIPE_SECRET_KEY=”your_key_here” DATABASE_URL=”postgres://user:pass@localhost:5432/db”

Then actual .env stays local.

And yes, .gitignore needs to exist before the mistake happens. But people still make mistakes, so we add more layers.

Rule #2: Put a guard at the door (pre-commit secret scanning)

This is one of the highest ROI moves you can make.

Pre-commit hooks scan what you’re about to commit. Not what’s already in the repo. This catches leaks at the moment they happen, when it’s easiest to fix.

Tools that work well:

  • gitleaks (very popular, lots of rules)
  • trufflehog (good detection, also scans history)
  • git-secrets (simple, classic)
  • pre-commit framework (to standardize hooks across teams)

The point isn’t which one you choose. The point is: every commit gets scanned automatically.

Because relying on “remember not to commit secrets” is not a strategy.

Also, enforce it in CI too. Because some people will skip hooks. Or not have them installed. CI doesn’t forget.

Rule #3: Use your deployment platform’s secret store

If you deploy anywhere modern, you already have a secret manager. You just might not be using it.

Examples:

  • GitHub Actions Secrets
  • GitLab CI variables
  • Vercel Environment Variables
  • Netlify environment variables
  • Render secrets
  • Fly.io secrets
  • Heroku config vars
  • AWS ECS task secrets, Lambda environment variables (with KMS), etc

The workflow should look like this:

  1. Store secrets in the platform
  2. Inject them as environment variables at runtime
  3. Read them from process.env or equivalent

So you never have to place them in the repo.

A good test is: can I deploy from a clean clone with no secret files present? If the answer is yes, you’re on the right track.

Rule #4: Use a real secrets manager for serious systems

Platform secret stores are good. But once you have multiple services, multiple environments, rotations, audits, and access policies, you want a dedicated tool.

The usual options:

  • AWS Secrets Manager or SSM Parameter Store
  • GCP Secret Manager
  • Azure Key Vault
  • HashiCorp Vault
  • 1Password Secrets Automation (nice for teams already on 1Password)
  • Doppler (developer friendly, popular)
  • Infisical (open source option many teams like)

What you get from a “real” secrets manager:

  • Centralized secrets across environments
  • Fine grained access control
  • Audit logs (who accessed what, when)
  • Rotation workflows
  • Versioning and rollback
  • Sometimes dynamic secrets (short lived creds)

A small team can absolutely start with platform secrets. But if you’re scaling, or handling regulated data, or you have more than one app and one environment… you’ll feel the pain eventually.

Rule #5: Separate dev, staging, production like you mean it

A lot of damage happens because the same key is used everywhere.

So when dev leaks, prod burns.

Do this instead:

  • Different secrets per environment
  • Different accounts or projects when possible (separate Stripe accounts, separate cloud projects)
  • Limit dev keys to dev resources only
  • Limit prod keys to prod resources only

If someone leaks a dev key, it should be annoying. Not catastrophic.

Also, avoid “super keys” that can do everything. Most platforms let you scope keys.

Examples:

  • Use restricted API keys where possible
  • Use read only tokens for dashboards
  • Use separate tokens for CI vs runtime
  • If you only need to send emails, don’t use a key that can also manage templates and delete lists

Least privilege is boring, and it’s also how you avoid waking up to a $12,000 surprise.

Rule #6: Stop putting secrets in frontend code (use a backend proxy)

Let’s say you’re calling a third party API from a web app and the API requires a secret key.

If you put that key in the browser bundle, it’s public. Even if you hide it in “obfuscated” code. Even if you think users won’t look.

The correct pattern is usually:

  • Browser calls your backend endpoint
  • Backend adds the secret key server side
  • Backend calls third party API
  • Backend returns only what the browser needs

Yes, it adds a hop. But it gives you:

  • A place to enforce auth
  • Rate limiting
  • Request validation
  • Logging without leaking secrets
  • Control over what’s exposed

And if you truly must use a client side key, use keys designed for that. For example, publishable keys, anonymous keys, public tokens with strict domain restrictions.

If it can’t be restricted and it can spend money, it doesn’t belong in the client.

Rule #7: Be careful with logs (they quietly leak everything)

Most teams leak secrets in logs more than in code. It just doesn’t get talked about because logs feel internal.

A few practical habits:

  • Never log full Authorization headers
  • Never log full request bodies by default
  • Mask known patterns in logs
  • Turn off verbose HTTP logging in production
  • Scrub secrets from exceptions before reporting to Sentry or similar
  • In CI, avoid set -x or printing env vars

Also, treat log platforms as semi public. Not because outsiders can see them, but because they’re widely accessible internally, exported, integrated, and kept forever.

If you log a secret, it becomes a data retention problem.

Rule #8: Rotation is not optional. Plan for it

Every secret you create should come with an assumption: it will leak one day.

So you need to be able to rotate it quickly. Like, calmly. Not like a disaster movie.

What makes rotation easier:

  • Secrets are stored centrally (one place to update)
  • Apps read secrets at startup or reload them safely
  • You have separate secrets per environment
  • You have a documented rotation process
  • You can deploy quickly

If you’re early stage, a manual rotation runbook is fine. Write it down anyway. Literally a checklist.

If you’re more mature, automate rotation for the most sensitive ones. Some cloud providers support automatic rotation for database passwords and such.

And keep an eye on “secret sprawl”. If the same key is copy pasted into five services and three cron jobs, rotation becomes a week long project. That’s how teams end up not rotating.

What to do if you already leaked a key (do this, not vibes)

This part matters because leaks happen even with good systems.

Here’s a sane sequence:

  1. Assume it’s compromised immediately
  2. Don’t debate. Don’t wait to see if anything bad happened.
  3. Revoke or rotate the key right away
  4. Prefer revoke if you’re not sure what uses it. Otherwise rotate with a new key and update systems.
  5. Search for where it was used
  6. Check provider logs, usage dashboards, recent API calls, billing anomalies, IP addresses if available.
  7. Purge it from history properly
  8. Deleting a line isn’t enough. You may need to rewrite Git history using tools like git filter-repo (preferred) or BFG.
  9. Then force push. Then tell everyone to re-clone. Yes it’s annoying. Still do it.
  10. Invalidate caches and artifacts
  11. CI artifacts, Docker images, build logs, release bundles. If the secret got baked into an image, rotating the key is good, but also fix the pipeline so it doesn’t happen again.
  12. Add prevention so it doesn’t repeat
  13. Pre-commit scanning, CI scanning, better .gitignore, better secret storage.

Also, if it’s a payment provider key (Stripe, etc), treat it as urgent. Attackers love those because it turns into money fast.

A simple setup that works for most teams (without overengineering)

If you’re thinking, okay but what should I actually implement this week, do this:

  1. Add .env to .gitignore, commit .env.example
  2. Use environment variables everywhere, no secrets in code
  3. Install gitleaks or trufflehog pre-commit hooks
  4. Add the same scanner to CI, fail builds on detection
  5. Store secrets in your deploy platform (or a secrets manager)
  6. Split dev and prod keys, with least privilege scopes
  7. Create a “rotate keys” doc, even if it’s short and ugly

That alone eliminates a huge percentage of common leaks.

The part people don’t like: humans need friction

A lot of security advice is framed as “don’t slow developers down”. Which I get. I build things too. I hate extra steps.

But secrets management needs a little friction.

Not everywhere. Not constantly. Just at the points where humans make mistakes.

  • Pre-commit scans are friction, but only when you’re about to do something dangerous.
  • CI scans are friction, but only when you’re about to ship something dangerous.
  • Central secret stores are friction, but they also remove the most error prone parts of handling secrets.

If you design it right, it’s less friction overall. Because you stop doing emergency rotations and retroactive cleanups.

Quick checklist to stop leaking API keys

Use this as a blunt self audit:

  • No secrets in source code, ever
  • .env files not tracked, .env.example committed
  • Pre-commit secret scanning enabled
  • CI secret scanning enabled
  • Secrets stored in platform secret store or a dedicated manager
  • Separate secrets for dev, staging, prod
  • Keys are scoped to least privilege
  • No secrets shipped to frontend or mobile apps
  • Logs scrubbed, no auth headers or tokens printed
  • Rotation process exists and is tested

If you get most of these right, you’re already in the top tier of “not leaking keys online”.

Not perfect. Just… not constantly on fire.

Wrap up

Leaking API keys online isn’t a morality play. It’s a systems problem.

If your process depends on every developer never making a mistake, the process is broken. People will commit the wrong file, paste the wrong thing, ship the wrong config. Eventually.

So build layers.

Keep secrets out of Git. Scan before commits. Scan in CI. Store secrets in proper places. Separate environments. Rotate keys like you expect compromise, because you should.

And if you take nothing else from this, take this.

A secret you can’t rotate quickly is not a secret. It’s a liability you’re renting.

FAQs (Frequently Asked Questions)

What types of information are considered ‘secrets’ beyond just API keys?

Secrets include more than API keys; they encompass OAuth client secrets, database connection strings with embedded credentials, private keys like SSH or service account JSON files, webhook signing secrets, session secrets, encryption keys, salts, internal tokens such as CI or deploy tokens, personal access tokens, and sensitive files like .p12 or .pem. Essentially, if it authenticates, signs, or grants access to data or funds, it qualifies as a secret.

How do most secret leaks actually occur in software development workflows?

Most leaks happen through common but often overlooked practices: hardcoding secrets directly in source code; committing .env or configuration files containing secrets to Git repositories; copying and pasting secrets into tickets, Slack messages, Notion pages, or screenshots; exposing secrets in CI logs and build outputs due to misconfiguration; shipping secrets in client-side code like frontend bundles; and assuming private repositories are completely secure when they might be accessible by contractors, backups, or compromised accounts.

Why is storing secrets in Git repositories risky even if deleted later?

Because Git retains history of all commits, any secret committed—even temporarily—remains accessible in the repository’s history. Additionally, platforms like GitHub may cache or index these commits quickly. This means that deleting a secret in a later commit does not remove it from the repository’s history or from external caches and indexes where bots or attackers might find it.

What are best practices for managing environment variables securely during local development and version control?

Use local .env files to store real secrets during development and load them with dotenv or framework support. Never commit real .env files; instead, commit a .env.example file containing dummy values and clear instructions. Ensure that .env and other secret files are added to .gitignore before any accidental commits happen. This approach keeps real secrets local while providing developers with necessary configuration templates without exposing sensitive data.

Why should secrets never be handled manually more than necessary?

Manual handling relies on human memory which is fallible and inconsistent (‘human memory with worse uptime’). Frequent manual processes increase the risk of accidental exposure through copy-paste errors, screenshots, logs, or misconfigured sharing. Automating secret management by using dedicated systems for injection at runtime reduces human error and enhances security by making leaking secrets harder than breathing.

Is relying on private repositories sufficient protection against secret leaks?

No. Private repositories reduce exposure but do not guarantee safety. Risks remain from contractors or former employees with access, dependency scanners that might expose data, backups that contain old versions of repos, accidental changes to visibility settings, mirrored forks potentially becoming public, and compromised accounts. Private means fewer eyes but not necessarily the right eyes—therefore additional security measures are essential.

Share it on:

Facebook
WhatsApp
LinkedIn