You have an OPENAI_API_KEY in a .env file somewhere. It probably starts with sk-proj-. It almost certainly lets the holder call any model on any endpoint, bill it to your org, and keep doing that until somebody notices the line item.
Now you are about to hand that key to an autonomous loop. Maybe it is a Cursor extension, a LangChain pipeline, a Claude Code skill, a scheduled cron job that drafts replies overnight, or an MCP server that a coworker installed because it looked cool. The agent will read tool outputs you did not write, follow instructions you did not sanction, and at some point will sit in a process whose memory anything on the box can read.
The question is not "should I be careful with API keys" (yes). The question is: how much of the blast radius can I shave off using only what OpenAI ships, before I reach for anything else? The answer in 2026 is "a surprising amount, if you actually use Projects and Restricted permissions." Here is the working map.
This post is the sibling to GitHub token hygiene for AI agents. The shape of the problem is the same, the controls are not.
The four key types OpenAI ships today
There are four prefixes in circulation. Treat the prefix as the first piece of access control, because the type determines what the key can do before you change a single setting.
| Prefix | Type | Can call models? | Can call Admin API? | Issued today? |
|---|---|---|---|---|
sk-proj- | Project key | Yes | No | Yes (default) |
sk-svcacct- | Service-account key | Yes | No | Yes |
sk-admin- | Admin key | No | Yes | Yes |
sk- (legacy) | User key | Yes | No | No |
sk-proj- became the default when Projects shipped in mid-2024. Legacy sk- keys still authenticate, but new ones are not issued. If you still have any, they are tied to a specific human's user account, share the whole org's billing pool, and have no project to attribute usage against. Rotate them out.
The most important and most underused fact in this list: admin keys cannot call inference endpoints. An sk-admin- key can create users, invites, projects, project API keys, service accounts, spend alerts, data retention overrides, and pull the audit log. It cannot hit /v1/chat/completions, /v1/responses, embeddings, audio, images, or any model. The SDKs even use a separate OPENAI_ADMIN_KEY env var. This separation is documented in OpenAI's Admin APIs guide. Keep your admin keys far away from anything an agent can read.
What a Project actually buys you
A Project in OpenAI is an isolated container inside an organization with its own:
- member list
- allowed-models list (allowlist or denylist per project)
- per-model rate limits (set below the org cap)
- API keys and service accounts
- usage attribution
The intended pattern, spelled out in the OpenAI help center, is one project per environment or surface area. So: support-bot-staging, support-bot-prod, internal-rag-prod, cursor-extension-eng. Each one gets its own keys, its own model allowlist, its own rate budget, and shows up as its own line in the usage dashboard.
The thing this isolation does NOT give you, and which people consistently get wrong: rate limits do not stack per key. All keys inside a project share that project's RPM/TPM/RPD/TPD. All projects inside an org share the org cap. Tier graduation (Free, Tier 1, ..., Tier 5) is driven by cumulative paid spend, not by how many keys you issue. The rate limits guide is explicit about this. Issuing five sk-svcacct- keys to five agents in the same project gives you five revocation handles, not five independent budgets.
If you want agents not to starve each other, give them separate projects, not separate keys.
Service-account keys: a project key that is not owned by a human
A service account is a pseudo-user that lives inside one project. Its key still starts sk-svcacct-. The point is that long-running background work, CI/CD runners, agents, scheduled jobs, none of these should depend on a specific employee's user account still existing when that employee leaves the company.
Service-account keys are documented in OpenAI's service-account API reference. Two operational facts worth memorising:
- The secret is shown once at creation. If you lose it, you rotate, you do not recover.
- There is a community-reported issue from 2025 of service-account keys silently disappearing without an
api_key.deletedaudit event. I cannot independently confirm OpenAI shipped a fix. Treat it as a community report, but it is a good argument for monitoring your runtime for 401s and alerting fast.
When you create one, the call looks like this. All three of these calls require an sk-admin- key, so this is something an operator or a bootstrap script does, not the agent itself.
export OPENAI_ADMIN_KEY=sk-admin-...
# 1. Create a project for the agent
curl https://api.openai.com/v1/organization/projects \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "support-bot-prod"}'
# returns { "id": "proj_abc123", ... }
# 2. Create a service account inside that project
curl https://api.openai.com/v1/organization/projects/proj_abc123/service_accounts \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "support-bot-runtime"}'
# returns { "api_key": { "value": "sk-svcacct-...", ... } }
# This is the ONLY time the secret is shown.
That secret is now the credential your agent will use. Before you hand it over, scope it.
Restricted permissions: the control nobody enables
When you create or edit a key in the platform UI, the Permissions selector has three modes: All, Restricted, and Read Only. In Restricted mode you set None / Read / Write for each endpoint group: Models, Chat Completions, Responses, Embeddings, Audio, Images, Files, Vector Stores, Fine-tuning, Assistants, Batch, Webhooks, and a few others. This is documented in OpenAI's permissions help article.
A chat-only agent needs almost none of these. A correctly scoped key for a Responses-API chatbot is:
| Endpoint group | Permission |
|---|---|
| Models | Read |
| Responses (or Chat Completions) | Write |
| Embeddings | None |
| Files | None |
| Fine-tuning | None |
| Assistants / Vector Stores | None |
| Audio / Images | None |
| Batch | None |
| Webhooks | None |
If a prompt-injection payload tricks that agent into trying to upload a file, list assistants, or kick off a fine-tuning job to exfiltrate context, OpenAI will reject the call with a 401-ish "this key does not have permission" error before any of it lands. That is real defence in depth, free, and almost nobody turns it on. Build the habit of treating "All" permissions as a code smell.
Restricted mode is the single highest-leverage thing on this list. If your agent only sends prompts and reads completions, it should have Models: Read and Responses: Write, and nothing else. Anything more is a footgun waiting for a prompt injection.
Calling the model with all three guards on
Once you have the key, pin it to the project explicitly on every request. The agent then runs with three layers of scope: the key's permissions, the project's model allowlist, and the org's rate ceiling.
export OPENAI_API_KEY=sk-svcacct-...
export OPENAI_PROJECT=proj_abc123 # pins requests to the project
export OPENAI_ORG_ID=org_xyz789 # only needed if the principal is in >1 org
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "OpenAI-Project: $OPENAI_PROJECT" \
-H "OpenAI-Organization: $OPENAI_ORG_ID" \
-H "Content-Type: application/json" \
-d '{ "model": "gpt-5", "input": "hello" }'
The OpenAI-Project header matters because a service-account key is already bound to one project, but if you ever reuse a multi-project admin-managed key or a legacy sk- key, the project header is the only thing telling OpenAI which budget to charge.
A decision matrix for picking the right key
| Use case | Key type | Scope | Why |
|---|---|---|---|
| Local dev on your laptop | sk-proj- (yours) | Restricted, dev project | Burnable, attributable to you, isolated from prod |
| Long-running production agent | sk-svcacct- | Restricted, per-agent project | Survives employee turnover, single revocation point |
| CI / batch job (scheduled summariser, embeddings ETL) | sk-svcacct- | Restricted to Batch + Embeddings only | Tight blast radius if the runner is compromised |
| Cursor / IDE extension on a teammate's box | sk-proj- (theirs) | Restricted, per-user project | One leak compromises one user, not the team |
| Provisioning, audit, spend-alert automation | sk-admin- | Lives on a control plane only | Cannot call inference, must never enter an agent process |
| Anything calling a third-party MCP server | sk-svcacct- | Minimum endpoints + dedicated project | Treat the MCP server as hostile by default |
The mental model: the type narrows what the key can do, the project narrows what it can spend, the permissions narrow what it can touch, and the audit log records who handed it out. Use all four.
What the dashboard and audit log actually log
Two endpoints to know.
Usage API. Keys created after 20 December 2023 have per-key usage tracking enabled by default. You can pull per-key cost attribution programmatically. The Usage API announcement covers the surface.
curl "https://api.openai.com/v1/organization/usage/completions?start_time=$(date -u -v-1d +%s)&group_by[]=api_key_id&group_by[]=model" \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY"
This is how you build "which agent is burning the most tokens" dashboards without manual tagging. It also catches a leaked key quickly: a spike from a key that runs a 10 req/min agent to 600 req/min is the first signal something is off.
Audit Logs API. GET https://api.openai.com/v1/organization/audit_logs (requires sk-admin-) emits api_key.created, api_key.updated, api_key.deleted, project.created, project.updated, project.archived, plus invites and role changes. The full event list is in the admin and audit logs help article. Ship it to your SIEM of choice (Datadog, Splunk, Chronicle, and similar tools generally accept JSON event streams).
curl "https://api.openai.com/v1/organization/audit_logs?event_types[]=api_key.created&event_types[]=api_key.deleted&limit=50" \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY"
The audit log records key and project lifecycle events, not individual inference requests. There is no "prompt log" you can pull, and no per-call attribution beyond what the Usage API already groups by api_key_id. If your compliance auditor asks for "what did this agent send to OpenAI", the answer has to come from your own application logs, not from OpenAI's.
What happens when a key leaks
OpenAI participates in GitHub's Secret Scanning Partner Program. When GitHub detects an OpenAI key in a public repo (or OpenAI detects one in a public app store), the key is auto-disabled and the org owner is emailed. GitHub has also progressively expanded secret scanning and push protection defaults on public repos, which catches a meaningful share of accidental commits. This is the good news.
The bad news is the scale of the problem this is fighting. Industry secret-sprawl reports from GitGuardian and similar trackers continue to log millions of new hardcoded secrets pushed to public GitHub each year, with AI-service credentials growing faster than the baseline. Specific year-over-year percentages vary by methodology and time window, but the direction is unambiguous: the agent that helps you write code is also helping you check secrets in.
If your key does leak, GitGuardian's remediation guide for OpenAI keys is a practical checklist: revoke immediately in the dashboard, rewrite git history if the commit is recent, audit usage on the affected key for anomalous spend, and look at what models and endpoints it was scoped to (this is where Restricted permissions repay you in minutes saved).
The runtime exposure problem OpenAI cannot solve
Here is the part where the dashboard ends and your architecture begins.
Every control I have described, project scoping, restricted permissions, admin/inference separation, usage attribution, audit logs, leak detection, all operate on the key as a string. They assume the string lives in a sensible place. They cannot help once the string is sitting in os.environ inside a Python process that is currently executing tool calls produced by a language model.
In 2025, multiple production tools were demonstrated to be coercible into exfiltrating environment variables and credentials:
- HiddenLayer published research on hidden-prompt attacks against AI code assistants including Cursor, where seemingly benign content in a workspace could redirect the assistant's behaviour.
- Lumenova documented a multi-agent prompt-injection demonstration where a single poisoned email led to credential exfiltration in a controlled test.
- An October 2025 advisory for the Amp AI Agent CLI described coercing the agent into DNS-tunneling environment variables to attacker-controlled servers without user consent.
In every one of these, restricted permissions still help (a key scoped to Responses-only cannot upload a file even if the attacker wants it to), and project isolation still helps (the blast radius is one project's allowlist, not the whole org). But the key itself is gone the moment the prompt-injected agent reads it. We covered the general pattern in how prompt injection becomes credential exfiltration.
This is the boundary where you stop tuning OpenAI's dashboard and start changing what the agent process actually holds.
Where a credential broker honestly fits
You have three architectural options for the runtime credential:
- Plaintext env var. The default. Easy, fast, and the agent literally holds the secret. Works fine for trusted backends, dangerous for tool-using LLMs and third-party MCP servers.
- Dynamic short-lived secrets. HashiCorp Vault's dynamic-secrets plugin for OpenAI issues a fresh, short-TTL key per lease. Strong if you already run Vault. Operational overhead is real.
- Local credential broker. The agent holds a placeholder. A local proxy on its egress path swaps in the real
Authorizationheader on the way out. A leaked agent leaks the placeholder.
Option 3 is where something like Authsome fits. The workflow is:
uv tool install authsome
authsome login openai # paste your scoped sk-svcacct- once
authsome run -- python my_agent.py
Inside my_agent.py, OPENAI_API_KEY is authsome-proxy-managed. The Python OpenAI SDK still sends Authorization: Bearer authsome-proxy-managed to api.openai.com. The local proxy intercepts the destination, swaps the header for the real sk-svcacct- from the encrypted SQLite vault under ~/.authsome/, and the request goes through. The agent never had the real key in memory at all. Every read is appended to a local JSONL audit log.
This is complementary to restricted project keys, not a replacement. You still want the underlying sk-svcacct- scoped to Responses-only, pinned to one project, with the project's model allowlist and rate caps set sanely. The broker narrows the window in which the secret string exists on the agent's side of the boundary. It does not, on its own, decide that a specific agent process should be denied a specific provider; that kind of per-agent policy enforcement is not something Authsome ships today.
It is also not the right answer for everything. A classic Rails API that calls OpenAI from a controller does not have an LLM in the loop and is no more exposed than any other backend service. .env plus Vault or SOPS is fine there. Brokers earn their keep specifically when an LLM or a third-party MCP plugin gets close to the credential.
A rotation cadence that does not depend on heroics
Pick a cadence and write it down somewhere your future self will find it.
- Quarterly rotation for production service-account keys. Use the Admin API to create the new key, deploy, then revoke the old one once you see traffic on the new one in the Usage API.
- On-demand rotation any time someone leaves, a laptop is lost, a key shows up in a log, or a Sentry trace contains an
Authorizationheader. - Daily monitoring of the Usage API per key. A 10x spike from a previously-stable key is the first sign of trouble, and you will often see it before any external leak-detection process flags the public commit.
- Audit log ingestion into whatever SIEM you already run.
api_key.createdfrom an unexpected admin, orproject.updatedadding a permissive model, are the events that matter. - Restricted by default. When you find an "All permissions" key that nobody can justify, rotate it to a scoped one. Treat "All" as the exception that needs sign-off.
The summary you can paste into your team's runbook
- Use
sk-proj-orsk-svcacct-, never legacysk-. Rotate any legacy keys you find. - One project per environment and per agent surface. Set the model allowlist and rate caps on the project, not on the key.
- Service-account keys for anything that runs without a human. Capture the secret on creation. Monitor for the silent-disappear bug.
- Always Restricted, never All. Read on Models, Write on Responses, None on everything else for a chat agent.
sk-admin-lives on a control plane. It cannot call inference. It must never be in an agent's environment.- Pin
OpenAI-Projecton every call. Log per-key usage daily. Ship the audit log to your SIEM. - For tool-using LLMs and third-party MCP servers, narrow the runtime exposure with a broker, dynamic secrets, or a local proxy. The restricted project key is still your baseline.
If you do those eight things, the next prompt injection that lands in your agent's context will buy the attacker model=gpt-5, input=..., attributable to one service account, in one project, with a 401 on anything else they try, and a Usage API dashboard that will show you the spike inside an hour. That is the bar.
Next steps
Quickstart
Run an OpenAI-calling agent under Authsome in three commands. The agent never holds the real sk-svcacct- key.
GitHub token hygiene for AI agents
The sibling deep-dive on GitHub's PAT, fine-grained, and GitHub App models, and which one to hand an agent.
How prompt injection becomes credential exfiltration
The attack pattern that makes restricted keys and credential brokers matter in the first place.
API key management for AI agents: 2026 guide
The cross-provider view: rotation, scoping, brokers, and audit across OpenAI, GitHub, and more.
Further reading
Compliance for AI agents: SOC 2, audit trails, and the credential question
SOC 2, ISO 27001, and the EU AI Act all assume a stable identity used a credential. AI agents break that assumption, and the gap between secrets-manager logs and the agent task that actually triggered the call is the part nobody ships for you.
Read postMay 29, 2026API key management for AI agents: the complete 2026 guide
A definitive 2026 guide to API key management for AI agents. The full options ladder, honest tradeoffs, a decision framework, and the threat model, with verified config for Cursor, LangChain, Doppler, Vault, and Infisical.
Read postMay 29, 2026Securing the OpenAI Agents SDK end to end
The OpenAI Agents SDK reads OPENAI_API_KEY from your process environment and gives tools direct access to whatever you hand them. Here is how to lock that down end to end in 2026.
Read post