playbooks / secrets

Keys Stay Home

Your API keys never meet the browser, never meet git, and get rotated the moment they slip.

20 minutes▲ severity if skipped: critical
The duck weighs in

duck@duckaudit

Show me where your keys live. If the answer includes the word component, sit down.

how it burns you

Scrapers watch public repos and client bundles around the clock, and a leaked key gets tried within minutes, not days. The favorite snack: an OpenAI or Stripe key pasted into frontend code by a helpful AI assistant. The bill or the breach lands on you either way.

1

Learn the one rule of NEXT_PUBLIC_

Any env var starting with NEXT_PUBLIC_ is baked into the JavaScript every visitor downloads. That prefix is a promise that the value is safe to publish. Payment keys, service keys, and AI keys never get that prefix, full stop.

bash
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co   # fine, public by design
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...               # fine, protected by RLS
OPENAI_API_KEY=sk-...                              # server only, no prefix
STRIPE_SECRET_KEY=sk_live_...                      # server only, no prefix
2

Proxy secret calls through the server

The browser asks your server, your server asks the paid API with the key. The key stays in server memory. This is the whole pattern, and it is about ten lines.

ts
// app/api/ai/route.ts: the key never leaves the server
export async function POST(req: Request) {
  const { prompt } = await req.json();
  const res = await fetch("https://api.openai.com/v1/responses", {
    method: "POST",
    headers: { Authorization: "Bearer " + process.env.OPENAI_API_KEY },
    body: JSON.stringify({ model: "gpt-5", input: prompt }),
  });
  return Response.json(await res.json());
}
3

Keep env files out of git forever

Ignore the files, commit an example instead, and check what history already knows. A secret that ever touched a commit is public property until rotated.

bash
# .gitignore
.env
.env.local
.env*.local

# commit this instead
cp .env.local .env.example  # then replace every value with a placeholder
4

Rotate like you mean it

Deleting a leaked key from code does nothing; the key itself still works. Go to the provider dashboard, issue a new key, deploy it, then revoke the old one. Order matters so your app never runs keyless.

5

Put a scanner in front of your commits

Gitleaks runs in a second and catches the paste before it becomes a rotation ceremony. Wire it as a pre commit hook and forget about it.

bash
brew install gitleaks
gitleaks git .           # scan history now
gitleaks protect --staged  # run this in a pre commit hook

Make your AI do it

Paste this into Cursor or Claude Code from your project root.

Audit this repo's secret handling. 1) List every environment variable read anywhere in the code, and classify each as public by design or server only. 2) Flag any server only value that is referenced in a client component, has a NEXT_PUBLIC_ prefix, or appears in code committed to git. 3) Check .gitignore covers all env files and generate a .env.example with placeholders. 4) For each flagged secret, write me the exact rotation steps for its provider. Do not print real secret values in your output, refer to them by variable name.

The 10 minute check

0/5

receipts · every claim has a source