Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~19 min· topic 4 of 9

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.

Step 3: keep it out of logsdiagram
Rendering diagram…

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. 1

    Unset the key

    In the first example, change the ENV dictionary to be empty. Which line of output changes, and does the program fail with a clear message or a confusing one?

  2. 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. 3

    Audit your own project

    Search a project of yours for the text api_key and Bearer. Is any key in code, in a .env file that is committed, or in a log statement?

Code & diagrams

Load, mask, redact and scan for keys New tab

The environment is a plain dictionary here so the example runs in the browser. The key-shaped strings are made up.

Sign in to run this example in your browser.

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:2
The proxy pattern: your server holds the keypython

Not 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.

terminal
$ Run
── what you'll see ──
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
log line: POST /v1/chat/completions Authorization: Bearer sk-live48271xyz9901
files with a key in them: config.py:2, notes.txt:2

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. 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 uses x-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. 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.NAME or System.getenv("NAME"). Local development uses a .env file that is listed in .gitignore and never committed.

  3. 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. 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. 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. 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

01

Why can't an API key be used in front-end JavaScript?

02

What is the proxy pattern and what does it add besides hiding the key?

03

Walk through what to do after discovering a key in a public repository.

Practice

01

Write mask(key) that returns the first 3 characters, ... and the last 4 characters, and show it on sk-abcdef123456.

02

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.