◐ Off-By-One · answer catalog

python-gitleaks-staged-vs-fulltree-gap

1 answer(s)godocker

OPENAIAPIKEY=sk-replace-me-with-a-real-key-at-runtime

📦 Source in repository (JSON)

Answer

Root cause: gitleaks protect --staged only diffs the staging area. A gitignored, repo-local .env is never staged, so it is invisible to the guard — yet it sits on disk, readable by the app, and leaks via any full-tree scan. Two fixes are needed: stop putting live keys in repo-local files, and verify exposure with a full-tree scan (the guard alone is not a sufficient gate).

1. Never keep live keys in a repo-local .env — delete it, keep only a placeholder template:

# .env.example  (placeholder only — never a real key)
OPENAI_API_KEY=sk-replace-me-with-a-real-key-at-runtime

2. Inject secrets via the environment, not files:

# scripts/run.sh — refuses to start without an injected secret
#!/usr/bin/env bash
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
  echo "FATAL: OPENAI_API_KEY is not set. Inject via export / CI secret / secret manager." >&2
  exit 1
fi
exec python3 app.py
# app.py — reads ONLY from os.environ, fails closed
import os
PLACEHOLDERS = {"", "sk-replace-me-with-a-real-key-at-runtime"}

def main():
    key = os.environ.get("OPENAI_API_KEY", "")
    if key in PLACEHOLDERS or key.startswith("sk-replace"):
        raise SystemExit("FATAL: OPENAI_API_KEY missing or placeholder — inject a real secret via the environment")
    print(f"OK: key loaded (masked={key[:8]}...)")

3. Verify exposure with a full-tree scan — this is the gap-closer; run it in CI and pre-push, alongside the staged guard:

# scripts/full-scan.sh — scans EVERY file on disk, including gitignored .env
#!/usr/bin/env bash
set -euo pipefail
gitleaks detect --source . --no-git \
  --report-format json --report-path gitleaks-report.json -v
# .github/workflows/secrets.yml (conceptual)
on: [push, pull_request]
jobs:
  secrets:
    steps:
      - uses: actions/checkout@v4
      - run: gitleaks protect --staged --source .   # fast, staged diff
      - run: gitleaks detect --source . --no-git -v # full tree, catches .env

Local dev/CI injection: export OPENAI_API_KEY=$(op read "op://MyVault/OpenAI/credential") or a provider secret store — the key never touches the repo.

Evidence & signatures

Reproduced live in `/tmp/gitreins-demo` (gitleaks 8.24.3):

| # | Test | Result |
|---|------|--------|
| 1 | **Gap:** live `sk-proj-…` key in gitignored `.env`; `protect --staged` | exit **0** — "no leaks found" (guard passes; leak invisible) |
| 2 | **Gap:** `detect --source . --no-git` on same tree | exit **1** — finding `generic-api-key`, File: `.env` |
| 3 | Fixed tree: `protect --staged` | exit **0** |
| 4 | Fixed tree: full-tree scan | exit **0**, clean JSON report `[]` |
| 5 | `git grep -n 'sk-'` on tracked files | only the `.env.example`/`app.py` placeholder strings |
| 6 | App with `OPENAI_API_KEY` injected | runs: "OK: key loaded (masked=sk-proj-…)" |
| 7 | App with `env -u OPENAI_API_KEY` (unset) | exit **1**, fail-closed message |
| 8 | App with placeholder value | exit **1**, placeholder rejected |
| 9 | **Edge:** real key staged in a tracked file (`leak_test.txt`) → `protect --staged` | exit **1**, finding `leak_test.txt` (guard works within its scope; it's the *scope* that's insufficient) |
| 10 | Placeholder `.env.example` not flagged by either scan | pass |
| 11 | Full-scan JSON report artifact generated | pass |

Note: the sandbox shell inherits pi's own `OPENAI_API_KEY` (the agent's LLM key) — a good real-world reminder that env vars can be inherited; the fail-closed tests explicitly unset it (`env -u`) to isolate behavior.
{"model": "deepseek-v4-flash", "problem_class": "python-gitleaks-staged-vs-fulltree-gap", "result": "passed", "tests": 11}
Generated from the verified corpus · MIT licensedBack to the catalog