Topic 2.4
API Keys and Secrets
In one line
An API key is a password that spends your money. Keep it in an environment variable on a server, never in browser code, the repository or logs, route browsers through your own endpoint, and rotate it the moment it leaks.
Think of it like this
A company credit card. Whoever holds the number can buy things, and you pay the bill. You would not write it on a poster, post it in a group chat or leave it in a shared drawer. You give it to the finance team, who spend on your behalf under rules, and you cancel it the moment it goes missing.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- API key
- A secret string that identifies and bills the caller. Treat it like a password.
- Environment variable
- A named value the operating system gives a running program, used for settings and secrets.
- Secret
- Any value that grants access: API keys, passwords, tokens, private keys.
- Proxy (backend for frontend)
- Your server endpoint that sits between the browser and the provider and holds the key.
- Rotation
- Replacing a key with a new one on a schedule or after a leak, then revoking the old one.
- Redaction
- Hiding secrets in logs by replacing most of the characters.
- Secret scanning
- A tool that searches code, history and logs for strings that look like keys.
Step by step
01Step 1: the bad version
A tutorial pastes api_key="sk-..." into the code. It works, then the file is committed. A public repository is scanned by bots within minutes, and the bill arrives later. The most common cause of a leaked key is exactly this.
02Step 2: read it from the environment
load_key reads the variable and raises a clear error when it is missing, instead of failing later with a confusing 401. The example prints only a masked form (sk-...9901).
03Step 3: keep it out of logs
A log line with the Authorization header contains the whole key. redact finds anything shaped like a key and replaces it with the masked form. Better still, never log headers at all; redaction is the safety net.
04Step 4: scan for leaks
scan walks files line by line and reports where a key-shaped string appears. It finds the key hard-coded in config.py and an old one left in notes.txt, and ignores app.py, which only reads the variable.
05Step 5: route browsers through your server
The proxy blocks show the endpoint: validate the input size, check the user and limit, add the key from the environment, set a timeout, return the provider's reply. The browser only knows your URL.
06Step 6: plan for the day it leaks
Write the steps down now: create a new key, deploy it, revoke the old one, check the provider's usage page for unknown spending, then find how it leaked. Practising the rotation once makes the real one calm.
Try it yourself
- 1
Unset the key
In the first example, change the
ENVdictionary to be empty. Which line of output changes, and does the program fail with a clear message or a confusing one? - 2
Change the pattern
Make the scanner pattern require 20 or more characters after
sk-. Which files are reported now? What risk does a stricter pattern add? - 3
Audit your own project
Search a project of yours for the text
api_keyandBearer. Is any key in code, in a.envfile that is committed, or in a log statement?
Code & diagrams
The environment is a plain dictionary here so the example runs in the browser. The key-shaped strings are made up.
Expected output
loaded key: sk-...9901
missing key: MODEL_API_KEY is not set: export it in the shell or the deployment settings
raw log line: POST /v1/chat/completions Authorization: Bearer sk-live48271xyz9901
redacted log line: POST /v1/chat/completions Authorization: Bearer sk-...9901
files with a key in them: config.py:2, notes.txt:2Not run here: framework sketches (FastAPI, a Next.js route handler, Spring Boot 3.2+ RestClient) that need a key and a server. Add authentication and rate limits before using one in public.
# FastAPI sketch: the browser calls /ask on YOUR server, which holds the key.
import os
import httpx
from fastapi import FastAPI, HTTPException, Response
app = FastAPI()
@app.post("/ask")
async def ask(payload: dict):
question = payload.get("question")
if not isinstance(question, str) or len(question) > 2000:
raise HTTPException(status_code=400, detail="bad request")
# check who is calling and how much they have used today (Topic 6.2), then call upstream
async with httpx.AsyncClient(timeout=30) as client:
r = await client.post(
"https://api.groq.com/openai/v1/chat/completions",
headers={"Authorization": "Bearer " + os.environ["GROQ_API_KEY"]}, # never sent to the browser
json={"model": "openai/gpt-oss-20b", "messages": [{"role": "user", "content": question}], "max_tokens": 400},
)
# pass the provider's status and body through, like the JavaScript version
return Response(content=r.text, status_code=r.status_code, media_type="application/json")
Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Log the whole header
In the Python example replace print("redacted log line: " + redact(line)) with print("log line: " + line), as a careless debug statement would.
Myth vs fact
Myth
Minifying or obfuscating the JavaScript hides the key.
Fact
Anything sent to a browser can be read in the network tab. Only a server-side call keeps a key secret.
Myth
A private repository makes it safe to commit keys.
Fact
Access changes, forks leak, and history is permanent. Keep secrets out of version control entirely.
Myth
If I delete the commit, the key is safe again.
Fact
Treat it as leaked: bots copy keys within minutes. Revoke and replace the key.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Use your platform's secret manager (cloud secret stores, Kubernetes secrets, Vercel or GitHub environment secrets) instead of files, and give each service access only to the secrets it needs.
- ▸
Set a monthly spending limit and an alert at a fraction of it, where your provider offers them. Check whether a limit blocks calls or only sends a notification. Cost spikes are often the first sign of a leaked key (Topic 6.2).
- ▸
In bring-your-own-key products, the user's key is their secret: encrypt it at rest, never log it, and never send it back to the browser after saving.
Remember this
- 1
Why this exists. The provider needs to know who is calling and whom to bill. The API key is that proof: a long random string sent in a header (usually
Authorization: Bearer ...; Anthropic usesx-api-key). It has no second factor. Anyone who has the string can make calls as you, up to your spending limit, and the provider will bill you. Attackers run bots that scan public code for keys within minutes of a leak. - 2
Where a key may live. In an environment variable (a named value the operating system gives to a program) on a server you control, set through your deployment platform's secret settings. Code reads it with
os.environ["NAME"],process.env.NAMEorSystem.getenv("NAME"). Local development uses a.envfile that is listed in.gitignoreand never committed. - 3
Where a key must never live. (1) Browser or mobile app code: anything shipped to a user's device can be read; "hidden" in minified JavaScript is not hidden. (2) The repository, including old commits, notebooks and screenshots. (3) Logs, error messages and analytics events: a failed-request log that prints headers leaks it. (4) Prompts and chat messages: the model can repeat what it sees. (5) Support tickets and chat threads.
- 4
The proxy pattern. A browser app that wants a model reply calls your server endpoint. Your server checks who the user is, applies limits (Topic 6.2), adds the key and calls the provider. The key never leaves the server. This site's own lab and AI tutor work exactly this way: you never send a key, and the server enforces a daily limit per user. The blocks below show the proxy in all three languages.
- 5
Use least privilege and limits. Create one key per app and environment (development, staging, production), so one leak does not expose everything and you can see which app spends. Set a spending cap and budget alerts in the provider's dashboard. Some providers allow restricting a key to certain models or projects.
- 6
Rotation and leak response. Rotate (replace) keys on a schedule, and immediately on a leak: create the new key, deploy it, then revoke the old one. Add a secret scanner to your pipeline (the example shows the idea with one pattern; real tools such as gitleaks or GitHub's secret scanning know hundreds of formats). Redact keys in logs: keep only a few characters so you can tell which key was used.
Explain it without notes
Why can't an API key be used in front-end JavaScript?
What is the proxy pattern and what does it add besides hiding the key?
Walk through what to do after discovering a key in a public repository.
Practice
Write mask(key) that returns the first 3 characters, ... and the last 4 characters, and show it on sk-abcdef123456.
Write require_env(env, names) that returns a list of the names missing or empty in the env dictionary.
Trade-offs
- ↔
A server proxy adds a hop and some code, and in return gives secrecy, limits and provider flexibility.
- ↔
Short-lived tokens limit the damage of a leak but need refresh logic; long-lived keys are simpler and riskier.
Done when you can
Done when you can read a key from an environment variable and fail clearly when it is missing.
Done when you can name five places a key must never appear.
Done when you can explain the proxy pattern and where limits belong.
Done when you can list the steps to take after a leak.