Managing secrets and API keys
Keep API keys and passwords out of your code and Git: load them from the environment, ignore .env, understand why frontends cannot hide secrets, scan for leaks and learn what to do in the first hour after one leaks.
A secret is any value that gives access: API keys, database passwords, JWT signing keys, cloud access keys, webhook secrets, private keys and tokens. Leaked secrets are one of the most common ways real incidents start, and the cause is usually mundane: someone committed a .env file to a public GitHub repository.
In this tutorial you will keep secrets out of code, load them safely in Python and in a Vite frontend, stop them reaching Git, scan for leaks, and learn what to do in the first hour after one leaks.
Prerequisites: Python 3.10 or newer, Git installed, a terminal and a small project folder (the Brew & Bean API works). All keys in this lesson are fake; use fake values while you practise.
What counts as a secret
Examples of secrets
| Secret | Example (fake) | What an attacker can do |
|---|---|---|
| Database URL with password | postgresql://app:S3cret@host/brew | Read or change all orders and customer data |
| Cloud access key | AKIA... (AWS style) | Create servers, read storage, run up a bill |
| Third-party API key | SMS or email service key | Send messages at your cost, impersonate your shop |
| JWT or session signing key | SECRET_KEY=... | Forge login tokens for any user |
| AI API key | Provider key for an LLM | Spend your quota or money |
| Webhook signing secret | Payment gateway secret | Fake payment confirmations |
| Private keys | SSH or TLS private key | Impersonate you or your server |
Not a secret: your public site URL, the public part of a key pair, a Firebase web config meant for browsers (though protect it with security rules), a publishable key that the provider designs for use in browsers. If the provider's docs say "publishable" or "public", it is meant to be seen. If they say "secret", it must never appear in client code.
The golden rule
Secrets do not belong in source code. Not in .py or .js files, not in comments, not in Git history, not in Docker images, not in screenshots or chat messages. Code is copied to many places (laptops, GitHub, CI, teammates). A secret that is only in the environment of the running server is in far fewer places.
The common pattern is the twelve-factor approach: keep configuration in environment variables, and give each environment (your laptop, test, production) its own values.
Step 1: load secrets from the environment
Setup
- 1In your project folder, create a virtual environment and activate it.
- 2Install the loader:
pip install python-dotenv. - 3Create a file named
.envin the project root (note the leading dot) with fake values. - 4Create
config.pyandshow_config.pyas shown below. - 5Run
python show_config.py.
DATABASE_URL=sqlite:///brewbean.db
SMS_API_KEY=dev-key-not-real
Plain NAME=value lines, no quotes or spaces needed. This file stays on your machine and is never committed.
# config.py
import os
from dotenv import load_dotenv
load_dotenv() # reads a local .env file if present; real environment variables win
def require(name: str) -> str:
value = os.environ.get(name)
if not value:
raise RuntimeError(f"Missing required setting: {name}")
return value
DATABASE_URL = require("DATABASE_URL")
SMS_API_KEY = require("SMS_API_KEY")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
require makes the app fail fast with a clear message when a setting is missing, instead of failing later in a confusing way. DEBUG defaults to false, so the safe option is what happens when nobody sets anything.
# show_config.py
import config
print("Database:", config.DATABASE_URL)
print("SMS key loaded:", bool(config.SMS_API_KEY))
print("Debug:", config.DEBUG)
Notice it prints whether the key is loaded, not the key itself. Never print secrets in logs.
Database: sqlite:///brewbean.db
SMS key loaded: True
Debug: FalseTry renaming .env and running again: you should get RuntimeError: Missing required setting: DATABASE_URL. In production you do not ship a .env file at all. You set the same variable names in your host's settings panel (Render, Railway, Vercel, Azure, AWS and others all have an environment variables or secrets section) or in your orchestrator's secret store.
Commit a template, not the secrets
Keep a second file named .env.example that lists every variable name with fake or empty values, and commit that. New teammates copy it to .env and fill in their own values. It documents what the app needs without exposing anything.
DATABASE_URL=
SMS_API_KEY=
DEBUG=false
Safe to commit: names only, no real values.
Step 2: keep secrets out of Git
Tell Git to ignore the real file before your first commit. Once a secret is committed, it lives in history even if you delete the file in a later commit.
Ignore the file
- 1Create or edit
.gitignorein the project root and add the lines below. - 2Run
git statusand confirm.envis not listed as a file to add, while.env.exampleis. - 3Check that nothing is already tracked:
git ls-files | grep -i env(Git Bash) should list only.env.example. In PowerShell usegit ls-files | Select-String env. - 4If
.envis already tracked, rungit rm --cached .env, commit that change, and then treat the secret as leaked (see the incident section).
.env
.env.*
!.env.example
*.pem
*.key
secrets/
.env.* ignores variants like .env.production, and !.env.example makes an exception so the template is committed. Also ignore private key files.
Step 3: frontends cannot keep secrets
Anything bundled into a React or Vite frontend is delivered to every visitor's browser. Anyone can open developer tools and read it. In Vite, only variables starting with VITE_ are exposed to your frontend code, which is a clear signal: never put a secret in a `VITE_` variable. The same is true for NEXT_PUBLIC_ in Next.js and REACT_APP_ in Create React App.
Client-side secrets
| Where the key lives | Safe? | Why |
|---|---|---|
.env read by the FastAPI server | Yes | Stays on the server |
VITE_SMS_API_KEY in .env | No | Bundled into the JavaScript everyone downloads |
Hard-coded in App.jsx | No | Same, and also committed to Git |
| Mobile app code | No | Apps can be unpacked and read; call your server instead |
The fix is a backend proxy: the frontend calls your own API (POST /orders), and your server calls the third-party service using its secret. Your server can also enforce who may call it and how often. This is especially important for paid AI APIs: a key in the browser lets strangers spend your money.
Step 4: scan for leaks
Humans forget, so automate. Secret scanners look through files and Git history for things that look like keys. Gitleaks and TruffleHog are free open-source scanners. GitHub also offers secret scanning and push protection, which blocks pushes containing known key formats (availability depends on the repository type and plan; check GitHub's documentation).
# Scan the repository, including its history (after installing gitleaks)
gitleaks detect --source . --verbose
# Newer gitleaks versions also accept:
gitleaks git --verboseIf a secret is found you get the file, line, commit and rule name. Install options are in the gitleaks README (Homebrew, Docker, or a release binary for Windows). Command names changed between versions, so run gitleaks --help to see which your version has.
Better still, stop secrets before they are committed with a pre-commit hook. The pre-commit tool runs checks on every git commit.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: <latest release tag, for example v8.xx.x>
hooks:
- id: gitleaks
Replace rev with a real release tag from the gitleaks releases page. Then run pip install pre-commit and pre-commit install. From then on, a commit that contains something that looks like a key is blocked.
Step 5: what to do when a secret leaks
Rotate first
Assume any secret that was ever pushed to a public place is already stolen. Bots scan public repositories continuously. Deleting the file or rewriting history does not make the leaked value safe again.
Incident steps, in order
- 1Revoke or rotate immediately. Log in to the provider (cloud console, SMS service, AI platform) and delete the key or generate a new one. For a database password, change it in the database.
- 2Deploy the new secret to your environment variables and restart the app.
- 3Check for misuse. Look at the provider's usage and billing pages, access logs and recent changes for activity you do not recognise.
- 4Remove it from the repository. Delete the file, and clean history if needed with a tool such as
git filter-repoor BFG Repo-Cleaner. Force-pushing rewritten history affects everyone who cloned, so coordinate with your team. This is tidying up, not the fix. - 5Find out how it happened and add the missing control:
.gitignore, a pre-commit hook, push protection. - 6Record it. Write a short note of what leaked, when, and what you did. If personal data was exposed, your organisation may have legal duties (for example under India's DPDP Act); inform whoever handles that.
Better places to store production secrets
Where secrets live in production
| Option | Good for | Notes |
|---|---|---|
| Host environment variables | Small apps | Simple; set in the platform's dashboard; limit who can view them |
| Secret managers (AWS Secrets Manager, Azure Key Vault, Google Secret Manager, HashiCorp Vault) | Teams and larger systems | Access control, audit logs, rotation, versioning |
| CI/CD secrets (GitHub Actions secrets) | Pipelines | Never echo them in logs; scope them to the repositories and environments that need them |
| Docker secrets / Kubernetes secrets | Containers | Do not bake secrets into images or ENV lines in a Dockerfile |
Good habits that apply everywhere: give each secret least privilege (a read-only database user for reports, an API key limited to one action or one IP range), use different secrets per environment (a leaked development key must not open production), rotate regularly and after staff leave, and set spending alerts or budgets on pay-per-use services so abuse is noticed early.
Secrets and AI tools
Do not paste real keys, passwords or customer data into AI chat tools or prompts, and be careful when AI coding assistants read your project folder: configure them to ignore .env. AI-generated code often hard-codes example keys; replace them with environment variables before committing. See *Security pitfalls in AI-written code* in Build with AI.
You are a security-minded code reviewer. Review the code below and find every hard-coded secret or configuration value that should be an environment variable (API keys, passwords, tokens, connection strings, signing keys). For each one, give the line, suggest an environment variable name, and rewrite the code to read it with a clear error if it is missing. Also give me a `.env.example` and the `.gitignore` lines I need. Do not invent real-looking secrets.
Language: [e.g. Python / Node]
Code (replace any real secrets with FAKE before pasting):
[paste code]What to expect: Check that the rewritten code has no default values that are real secrets and that nothing secret is printed or logged. Always replace real keys with fake ones before sharing code with any AI tool.
How this comes up in interviews
Typical questions: "Where do you store API keys?" (environment variables or a secret manager, never in code or Git). "What do you do if a key is committed by mistake?" (rotate first, then clean up and add prevention). "Can a React app hide an API key?" (no; use a backend proxy). "What is secret rotation?" A strong answer also mentions least privilege, separate environments and scanning.
Try it: Leak drill on a practice repo
Create a new practice Git repository. Add a file settings.py containing the fake line SMS_API_KEY = "sk-test-FAKE1234567890", commit it, then run gitleaks on the repository. Next, move the value into .env, load it with config.py, add .gitignore, and commit. Run gitleaks again and note whether the old commit is still reported. Finally write down the steps you would take if the key were real.
Secrets checklist
- No secrets in source code, comments or Dockerfiles.
.envis in.gitignore;.env.exampleis committed.- No secrets in
VITE_,NEXT_PUBLIC_orREACT_APP_variables. - Different secrets for development and production.
- A secret scanner runs locally (pre-commit) and in CI.
- Keys have least privilege and spending alerts where possible.
- You know the revoke, redeploy, check, clean steps.
Key takeaways
- A secret is anything that grants access; keep it out of code and Git.
- Load secrets from environment variables and fail fast when one is missing.
- Commit
.env.example, ignore.env. - Frontend code is public: never put secrets in
VITE_or similar variables; call your backend instead. - Scan for leaks with gitleaks or GitHub secret scanning, and block them with pre-commit hooks.
- If a secret leaks, rotate it first, then check for misuse and clean up.
Check yourself
Finishing the quiz marks the lesson as done.
1. You accidentally pushed a real API key to a public repository, then deleted the file in the next commit. What is the right conclusion?
2. Why must a secret never be placed in a variable named VITE_API_KEY?
3. What is the purpose of a .env.example file?
4. How should a React app use a paid third-party AI API safely?
5. Why use a different database password for development and production?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.