5 minutes. That’s how long it took.

A security researcher publishes an AWS access key on a public GitHub repository. They do it on purpose, as an experiment.

Five minutes later, someone was already using it to mine cryptocurrency.

Five. Minutes.

There are bots scanning GitHub 24/7 looking for exactly that: exposed credentials. And they’re fast. Much faster than you realizing you screwed up.

The numbers are scary

According to GitHub, 39 million secrets were leaked in public repositories in 2024. A 67% increase from the previous year.

GitGuardian, which specializes in scanning exactly this, found 23.7 million new secrets just in public repos. And the worst part: 70% of secrets detected in 2022 were still active in 2024.

Two years later. Still working. Waiting for someone to use them.

It’s not just random people

Toyota had AWS credentials exposed on GitHub that gave access to their vehicle telematics system. Pearson lost data because someone left a GitLab token in a configuration file. Otelier, a hospitality company, watched as 8TB of S3 data was exfiltrated due to credentials exposed on Bitbucket.

This doesn’t just happen to the intern. It happens to Fortune 500 companies.

The classic: “It’s just my personal project”

Yeah, right.

The problem is that personal project has the same OpenAI API key you use in production. Or your Telegram bot token. Or your staging database credentials that, oh surprise, has real data because “it’s easier to test that way”.

And one day you do git push without thinking. Or you change the repo from private to public because you want to show it to someone. Or GitHub has a bug and temporarily exposes private repos (it’s happened).

And then you discover your AWS bill went from $20 to $2,000. In one night.

“But I deleted it immediately”

Another classic.

Git is a version control system. Its literal job is to remember everything that happened. Deleting the commit doesn’t delete the secret from history. Force pushing doesn’t delete it from forks. And it definitely doesn’t delete it from bots that already copied it.

Once a secret touches a public repo, it’s compromised. Period. It has to be rotated.

The pyramid of disaster

This is how secret management typically evolves in a typical project:

Level 1: Hell

1
2
3
# config.py
AWS_KEY = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

Directly in the code. Committed. In production. Don’t laugh, this exists.

Level 2: Purgatory

1
2
3
# .env (supposedly in .gitignore)
AWS_KEY=AKIAIOSFODNN7EXAMPLE
AWS_SECRET=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Better, but that .env ends up in a backup, in a zip you send via Slack, on a hard drive you sell on Craigslist…

Level 3: Limbo

1
2
# Secrets in system environment variables
export AWS_KEY=...

Okay, but where do you store them? On a sticky note? In a secrets.txt file on the desktop? In a Slack message to yourself?

The solution: secrets outside code, outside disk

After seeing how I almost screwed up a few times, I decided to centralize everything in 1Password and use its CLI to inject secrets when needed.

The concept is simple:

  1. Secrets live in 1Password, not on my disk
  2. Code has references to secrets, not the secrets themselves
  3. Secrets are injected at runtime

The .env.template + op inject pattern

In each project, instead of a .env with real values, I have a .env.template with references:

1
2
3
4
5
6
7
# .env.template - THIS GOES TO GIT
# Regenerate with: op inject -i .env.template -o .env.local

OPENAI_API_KEY=op://FRR DEV/OpenAI/api-key
SUPABASE_URL=op://FRR DEV/Supabase Qinqin/url
SUPABASE_SERVICE_KEY=op://FRR DEV/Supabase Qinqin/service-key
DATABASE_URL=postgres://localhost/mydb  # non-secret values go direct

When I need the real .env, I run:

1
op inject -i .env.template -o .env.local

1Password reads the op:// references, resolves them with real values, and generates the file. The .env.local is in .gitignore, it never touches git.

What do I gain from this?

1. One place for all secrets

Before, I had credentials scattered across .env files in 15 projects, environment variables in .bashrc, tokens in Slack messages, and a few on sticky notes (don’t judge me).

Now everything is in a 1Password vault. One place. Encrypted. With change history.

2. Trivial secret rotation

Before: changing an API key meant searching for it in all projects, updating files, praying I didn’t forget any.

Now: update the value in 1Password, run op inject in each project, done.

3. Code documents what it needs

The .env.template is living documentation. Anyone who clones the project knows exactly what secrets they need. They just need to have them in their own 1Password (or ask you for them).

4. Impossible to commit secrets by accident

The file that goes to git only has op:// references. Even if you do git add . without thinking, you’re not exposing anything.

5. Free synchronization between machines

New Mac? Install 1Password, log in, op inject. All your secrets available without copying files or sending anything via Slack.

For daily use: lazy loading in Fish

For commands that need credentials (like oco for AI commits), I have this in my Fish config:

1
2
3
4
5
6
function oco
    if not set -q OPENAI_API_KEY
        set -gx OPENAI_API_KEY (op read "op://FRR DEV/OpenAI/api-key")
    end
    command oco $argv
end

The first time I run oco, it asks for Touch ID. From then on, the variable is loaded in the session.

The only downside

Yes, there’s one: you have to authorize with Touch ID or password from time to time.

I’ve optimized it by configuring 1Password to remember authorizations for 24 hours and only lock when the Mac sleeps. But yes, occasionally you have to touch the sensor.

It’s a small price to pay for not appearing in GitGuardian statistics.

It’s not optional

Look, I understand this all sounds like paranoia. “It won’t happen to me.” “It’s just a small project.” “I don’t have anything important.”

But think about this: do you have any paid API keys? OpenAI? AWS? Any service that charges per usage?

Then you have something someone can exploit.

And bots don’t rest. They don’t distinguish between a startup’s project and a student’s programming homework. They scan everything, try everything, exploit everything.

39 million secrets leaked in 2024. 70% of those from 2022 are still active.

Proper secret management isn’t a best practice. It’s basic hygiene.

Like washing your hands. You don’t do it because it’s fun. You do it because the alternative is catching an infection.

Your secrets in git are an infection waiting to happen.


Executable summary:

  1. Install 1Password and its CLI (brew install 1password-cli)
  2. Create a vault for development
  3. Migrate your secrets from .env files to the vault
  4. Create .env.template with op:// references
  5. Add .env.local to .gitignore
  6. Regenerate with op inject when needed

That’s it. Thirty minutes of setup that saves you from appearing in the next GitGuardian report.

Your AWS credentials will thank you.


Update: After implementing all this, I discovered that 1Password was asking for Touch ID too much. So much that I started approving without looking. That has a name in security and it’s not good. Read When security asks for permission so often you stop reading to see how we solved it.