API Key Security
Protecting API keys and application secrets
An API key is a bearer credential: whoever holds the string can act as your account. When you call client.responses.create(...), the OpenAI SDK attaches your key to every request's Authorization header. There is no second factor, no password prompt, no additional check — possession of the key is proof enough. This is what makes API keys convenient for automated systems and, at the same time, what makes them dangerous if handled carelessly.
Unit 1, Lesson 3 covered the mechanics of creating a key and storing it as the OPENAI_API_KEY environment variable so the SDK could find it automatically. That was the minimum needed to get a working setup. This lesson treats key protection as an ongoing discipline rather than a one-time setup step, because a key that is safe on day one can easily become exposed on day thirty through a careless commit, a shared screenshot, or a misconfigured log.
Why API keys deserve special treatment
A leaked API key is not merely an inconvenience. Depending on your account configuration, someone who obtains your key can:
- Consume your billing quota. Every request made with your key is billed to your account, regardless of who sent it. A leaked key posted publicly can be picked up by automated scrapers within minutes and used to generate thousands of dollars in usage before you notice.
- Access data flowing through your account. If your organization has usage policies, fine-tuned models, or vector stores tied to the key's project, an attacker with the key can potentially read or manipulate that data.
- Impersonate your application. Requests made with your key are indistinguishable from your own legitimate traffic, which makes abuse harder to detect and can trigger rate limits or account flags that affect your real users.
This is fundamentally different from, say, a bug in your UI. A UI bug affects your own users. A leaked key can be exploited by anyone on the internet who finds it, and the damage accrues directly to your account and your bill.
The principle of least privilege for keys
Least privilege means giving a credential exactly the access it needs and no more. In the OpenAI platform, this shows up in two practical decisions:
- Use project-scoped keys, not a single organization-wide key. If your OpenAI organization has multiple projects (for example, one per environment or per service), create a separate API key per project. A key scoped to a "staging" project cannot touch production resources, so a leak in staging does not automatically compromise production.
- Give each service or environment its own key, rather than sharing one key across your local development machine, your CI pipeline, and your production servers. If a key is ever compromised, you can revoke that one key without disrupting everything else, and you can trace unusual usage back to the environment it came from.
This is why storing the key in OPENAI_API_KEY (Unit 1, Lesson 3) is only the starting point — the key that variable holds should already be the right key, scoped to the right project, for the environment it runs in.
Loading the key correctly at runtime
The SDK reads OPENAI_API_KEY automatically, but it is worth being explicit about how your application obtains it, because implicit behavior is easy to get wrong when you later introduce multiple keys or a secret manager (covered in Lesson 3 of this unit).
import os
from openai import OpenAI
def build_client() -> OpenAI:
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise RuntimeError(
"OPENAI_API_KEY is not set. Refusing to start without a valid key."
)
# The SDK also accepts the key implicitly, but passing it explicitly
# makes the dependency visible and testable.
return OpenAI(api_key=api_key)
This function does two things beyond OpenAI(): it fails loudly if the key is missing, and it makes the credential an explicit, visible dependency of build_client. Failing loudly matters because a silently missing key would otherwise surface later as a confusing authentication error deep inside a request, possibly after your application has already accepted user traffic.
Note: Some teams prefer to let
OpenAI()read the environment variable implicitly and only wrap it intry/exceptat the call site. Either approach is acceptable; what matters is that a missing or invalid key is detected before your application serves requests, not silently during them.
Never let a key reach places it shouldn't
A key is only as safe as everywhere it travels. Beyond the source-code and repository risks covered in the next lesson, keep the following boundaries in mind:
- Never print or log the raw key, even in debug output. A
print(api_key)left in during development is easy to forget and easy to accidentally ship. - Never send the key to the client/browser. If you build a web or mobile application, the API key must live on your backend server only. The frontend should call your backend, and your backend should call OpenAI — the key never crosses that boundary.
- Never embed the key in error messages that might be shown to users or sent to third-party error-tracking services without redaction.
Here is a small, testable helper that centralizes key handling and makes accidental exposure structurally harder:
import os
class SecretString:
"""Wraps a secret so it can't be accidentally printed or logged."""
def __init__(self, value: str):
if not value:
raise ValueError("Secret value must not be empty.")
self._value = value
def reveal(self) -> str:
"""Explicit, intentional access to the raw secret."""
return self._value
def __repr__(self) -> str:
return "SecretString(***redacted***)"
def __str__(self) -> str:
return "***redacted***"
def load_api_key() -> SecretString:
raw = os.environ.get("OPENAI_API_KEY")
if not raw:
raise RuntimeError("OPENAI_API_KEY is not set.")
return SecretString(raw)
The SecretString class does not make leaking impossible — nothing in Python can fully prevent that — but it changes the default behavior. If someone accidentally does print(api_key) or an error handler serializes the object into a log line, they get ***redacted*** instead of the real key. Only a deliberate call to .reveal() produces the actual value, which makes exposure a conscious act rather than an accident.
A simple test, using dependency injection so no real key or network call is involved:
def test_secret_string_hides_value():
secret = SecretString("sk-fake-example-key-123")
assert str(secret) == "***redacted***"
assert repr(secret) == "SecretString(***redacted***)"
assert secret.reveal() == "sk-fake-example-key-123"
print("PASS: SecretString redacts by default and reveals explicitly")
def test_load_api_key_missing(monkeypatch):
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
try:
load_api_key()
raised = False
except RuntimeError:
raised = True
assert raised
print("PASS: load_api_key raises when the key is missing")
test_secret_string_hides_value()
The second test uses monkeypatch (a pytest fixture) to simulate an unset environment variable without touching your real shell environment — this is the same dependency-injection idea applied to environment state rather than to an object.
When to rotate a key
Rotation means generating a new key and retiring the old one. You should rotate:
- On suspicion of exposure — a key committed to a repository, printed in a shared log, or pasted into a support ticket.
- On a routine schedule for high-value production keys, even without evidence of a leak, as a defense against undetected exposure.
- When an employee or contractor with access to the key leaves the project.
Rotation is only safe if your application reads the key from configuration (an environment variable or secret manager) rather than having it baked into a deployed artifact — another reason the pattern in Lesson 3 of this unit matters for real deployments.
Common Mistakes
- Hardcoding the key "just for a quick test" and forgetting to remove it. A key typed directly into a script for a five-minute experiment is easy to leave behind, and it survives in your shell history and possibly in a committed file. Always load it from the environment, even for throwaway scripts.
- Reusing one key across every environment. Development, staging, and production sharing a single key means a leak anywhere compromises everywhere, and you lose the ability to tell which environment generated a given request.
- Treating the key as safe once it's "just in an environment variable." Environment variables can still leak through process listings, crash dumps, misconfigured logging of
os.environ, or a debugging endpoint that echoes configuration. The environment variable is a safer home than source code, not an unconditionally safe one.
Best Practices
- Scope keys per project and per environment, and name them descriptively in the OpenAI dashboard so you can tell at a glance which key belongs where.
- Fail fast on a missing key rather than letting your application start in a broken state that only surfaces when the first request fails.
- Wrap secrets in a type that resists accidental printing, as shown with
SecretString, so that logging or debugging code cannot casually expose the raw value. - Rotate keys proactively, not only reactively, and make sure your deployment process supports swapping a key without a code change.