n4nAI

How to keep your OpenAI API key secure

A step-by-step guide to securing OpenAI API keys in production — environment variables, secrets managers, rotation, monitoring, and gateway-level controls.

n4n Team5 min read1,048 words

Audio narration

Coming soon — every post will get a voice note here.

You need to keep OpenAI API key secure from the moment you create it. A leaked key means unauthorized usage, unexpected bills, and potential data exposure if your requests contain sensitive context. This guide walks through the complete lifecycle: generation, storage, rotation, monitoring, and revocation. Each step includes verification commands so you can confirm the control actually works.

Step 1: Generate the key in a clean environment

Create the key on a machine you trust. Avoid CI runners, shared jump hosts, or browsers with extensions that log network traffic. If you must use a browser, open an incognito window, disable extensions, and verify the URL is https://platform.openai.com.

# Example: generate a key via the CLI (requires openai-python >= 1.0)
openai api keys.create --name "prod-inference-$(date +%Y%m%d)"

Verify: The output shows a key prefixed sk- exactly once. Copy it immediately to your secrets manager (Step 2). Do not write it to a file, clipboard history, or chat.

Step 2: Store the key in a secrets manager, not environment files

Environment variables are better than hardcoded strings, but .env files checked into git or baked into Docker images are a common leak vector. Use a dedicated secrets manager.

AWS Secrets Manager

aws secretsmanager create-secret \
  --name prod/openai/api-key \
  --description "OpenAI API key for production inference" \
  --secret-string '{"api_key":"sk-..."}'

Retrieve at runtime (example in Python):

import boto3
import json

def get_openai_key() -> str:
    client = boto3.client("secretsmanager", region_name="us-east-1")
    resp = client.get_secret_value(SecretId="prod/openai/api-key")
    return json.loads(resp["SecretString"])["api_key"]

GCP Secret Manager

echo -n "sk-..." | gcloud secrets create openai-api-key --data-file=-
from google.cloud import secretmanager

def get_openai_key() -> str:
    client = secretmanager.SecretManagerServiceClient()
    name = "projects/my-project/secrets/openai-api-key/versions/latest"
    resp = client.access_secret_version(request={"name": name})
    return resp.payload.data.decode("utf-8")

HashiCorp Vault (KV v2)

vault kv put secret/openai api_key=sk-...
import hvac

def get_openai_key() -> str:
    client = hvac.Client(url="https://vault.internal", token=os.environ["VAULT_TOKEN"])
    resp = client.secrets.kv.v2.read_secret_version(path="openai")
    return resp["data"]["data"]["api_key"]

Verify: Run your retrieval code in a fresh shell with no OPENAI_API_KEY set. Confirm it returns the key and your application starts successfully.

Step 3: Inject the key at runtime, never at build time

Build-time injection (Docker ARG, ENV in Dockerfile, GitHub Actions env in the job) writes the secret into image layers or workflow logs. Inject only when the process starts.

Kubernetes: use the Secrets Store CSI Driver or External Secrets Operator

# ExternalSecret example
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: openai-key
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secretsmanager
    kind: ClusterSecretStore
  target:
    name: openai-credentials
    creationPolicy: Owner
  data:
    - secretKey: api_key
      remoteRef:
        key: prod/openai/api-key
        property: api_key

Pod spec:

env:
  - name: OPENAI_API_KEY
    valueFrom:
      secretKeyRef:
        name: openai-credentials
        key: api_key

Docker Compose (local dev only): use a .env file that is gitignored

# docker-compose.yml
services:
  api:
    image: my-app:latest
    env_file:
      - .env.local   # not committed
# .env.local (chmod 600)
OPENAI_API_KEY=sk-...

Verify: docker inspect <container> shows no OPENAI_API_KEY in Config.Env. kubectl get secret openai-credentials -o yaml shows base64 data only in the cluster, not in your repo.

Step 4: Restrict the key with IP allowlists and usage limits

OpenAI lets you restrict keys to specific IP ranges and set monthly spend limits. Do both.

  1. In the OpenAI dashboard, open the key details.
  2. Add your egress IPs (NAT gateway, VPN, office). Use CIDR notation.
  3. Set a hard limit (e.g., $500/month) and a soft alert at 80%.
# No CLI for this yet; use the dashboard or the platform API
curl -X POST https://api.openai.com/v1/api_keys/sk-.../restrictions \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"allowed_ips": ["203.0.113.0/24"], "monthly_budget_usd": 500}'

Verify: From a non-allowlisted IP, curl -H "Authorization: Bearer sk-..." https://api.openai.com/v1/models returns 401. From an allowlisted IP, it returns 200. Check the usage dashboard after a test request — spend should increment.

Step 5: Rotate keys on a schedule and after any suspected exposure

Rotation limits the blast window of a compromised key. Automate it.

Rotation script (run weekly via cron or scheduled workflow)

#!/usr/bin/env python3
import os
import boto3
import openai

def rotate_key():
    admin_key = os.environ["OPENAI_ADMIN_KEY"]  # a separate key with key-management scope
    client = openai.OpenAI(api_key=admin_key)

    # 1. Create new key
    new_key = client.api_keys.create(name=f"prod-inference-{datetime.utcnow():%Y%m%d}")
    new_secret = new_key.api_key

    # 2. Update secrets manager
    sm = boto3.client("secretsmanager", region_name="us-east-1")
    sm.put_secret_value(
        SecretId="prod/openai/api-key",
        SecretString=json.dumps({"api_key": new_secret})
    )

    # 3. Verify new key works
    test_client = openai.OpenAI(api_key=new_secret)
    test_client.models.list()  # raises if invalid

    # 4. Revoke old key (get ID from secret metadata or tag)
    old_key_id = get_current_key_id()  # implement: tag the key at creation
    client.api_keys.delete(old_key_id)

    print(f"Rotated to {new_key.id[:8]}...")

if __name__ == "__main__":
    rotate_key()

Verify: After rotation, the old key returns 401 on /v1/models. The new key returns 200. Secrets manager version increments. Deployment picks up the new version (restart pods or rely on CSI driver refresh).

Step 6: Monitor usage in real time and alert on anomalies

You need visibility into who is using the key, what models, and how many tokens. OpenAI’s dashboard is delayed. Build your own pipeline.

Option A: Proxy through a gateway that logs per-request metadata

If you route traffic through n4n.ai, you get per-token metering, automatic fallback, and structured logs without changing your client code — just point the base URL to the gateway endpoint. The gateway forwards provider cache-control hints and honors your routing directives.

# Client code unchanged except base_url
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url="https://api.n4n.ai/v1"  # gateway endpoint
)

Option B: Middleware in your application

import time
import logging
from openai import OpenAI
from functools import wraps

usage_log = logging.getLogger("openai.usage")

def log_usage(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        resp = fn(*args, **kwargs)
        latency_ms = (time.perf_counter() - start) * 1000

        # Extract usage from response (chat.completions.create returns usage)
        usage = getattr(resp, "usage", None)
        usage_log.info(
            "openai_request",
            extra={
                "model": kwargs.get("model"),
                "prompt_tokens": usage.prompt_tokens if usage else None,
                "completion_tokens": usage.completion_tokens if usage else None,
                "total_tokens": usage.total_tokens if usage else None,
                "latency_ms": latency_ms,
                "endpoint": fn.__name__,
            }
        )
        return resp
    return wrapper

client = OpenAI(api_key=get_openai_key())
client.chat.completions.create = log_usage(client.chat.completions.create)

Ship logs to your observability stack (Datadog, Splunk, Loki). Alert on:

  • Sudden spike in total_tokens per minute
  • Requests from unknown user agents or IPs
  • 401/429 error rate > 1%

Verify: Trigger a test burst. Confirm your dashboard shows the spike and alert fires within your SLA (e.g., 5 minutes).

Step 7: Enforce client-side validation and request signing

Prevent malformed or oversized requests from reaching OpenAI. This reduces waste and blocks some injection attempts.

from pydantic import BaseModel, Field, validator
from typing import List, Optional

class ChatMessage(BaseModel):
    role: str = Field(pattern="^(system|user|assistant|tool)$")
    content: str = Field(max_length=100_000)  # hard cap

class ChatRequest(BaseModel):
    model: str = Field(pattern="^(gpt-4|gpt-4-turbo|gpt-3.5-turbo).*$")
    messages: List[ChatMessage] = Field(min_items=1, max_items=100)
    temperature: Optional[float] = Field(default=0.7, ge=0, le=2)
    max_tokens: Optional[int] = Field(default=None, ge=1, le=4096)

    @validator("messages")
    def total_chars(cls, v):
        total = sum(len(m.content) for m in v)
        if total > 200_000:
            raise ValueError("Combined message length exceeds 200k chars")
        return v

# In your handler
@app.post("/chat")
async def chat(req: ChatRequest):
    resp = client.chat.completions.create(**req.dict())
    return resp

Verify: Send a request with max_tokens: 100000 — expect 422. Send a 300k-char message — expect 422. Valid request returns 200.

Step 8: Implement a revocation procedure for confirmed leaks

When (not if) a key leaks, you need a runbook that takes < 5 minutes.

Revocation runbook

  1. Revoke immediately in OpenAI dashboard or via API:
    curl -X DELETE https://api.openai.com/v1/api_keys/sk-... \
      -H "Authorization: Bearer $ADMIN_KEY"
  2. Rotate using the script from Step 5.
  3. Audit the last 24h of logs for unauthorized requests (filter by key ID if your gateway logs it).
  4. Notify security team and, if applicable, customers whose data may have been sent.
  5. Post-incident: add the leaked key’s fingerprint to a blocklist in your gateway/WAF.

Verify: After revocation, the old key returns 401. New key works. Blocklist returns 403 for any request bearing the leaked key (test with a staged request).

Step 9: Audit key inventory quarterly

Keys accumulate. Service accounts, demo projects, former employees. Schedule a quarterly review.

# List all keys via admin API
curl -H "Authorization: Bearer $ADMIN_KEY" https://api.openai.com/v1/api_keys | jq '.data[] | {id, name, created, last_used}'

Checklist per key:

  • Owner identified (team, service)
  • IP allowlist current
  • Spend limit appropriate
  • Last used < 30 days ago (otherwise revoke)
  • Stored in secrets manager with correct IAM

Verify: Produce a markdown report committed to your internal docs repo. Action items tracked in your issue tracker.

Step 10: Train developers on the “no key in code” rule

Tooling prevents accidents; culture prevents workarounds.

  1. Add a pre-commit hook that scans for sk- patterns:
    # .pre-commit-config.yaml
    - repo: https://github.com/gitleaks/gitleaks
      rev: v8.18.0
      hooks:
        - id: gitleaks
  2. Add a CI job that fails on secret detection:
    # .github/workflows/secrets.yml
    jobs:
      secret-scan:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: gitleaks/gitleaks-action@v2
  3. Document the approved path: “Need a key? Ask #platform. We provision via Terraform and inject via CSI driver.”

Verify: Attempt to commit a file containing sk-test123. Pre-commit blocks it. Push anyway — CI fails. The only way to merge is to remove the secret and use the approved flow.


Summary checklist

Step Control Verification
1 Generate in clean env Key appears once, copied to secrets manager
2 Store in secrets manager Retrieval works from fresh shell
3 Inject at runtime No key in image layers or build logs
4 IP allowlist + spend limit 401 from blocked IP; budget enforced
5 Automated rotation Old key revoked, new key works, secret version bumped
6 Real-time monitoring Alert fires on test spike within SLA
7 Request validation Oversized/malformed requests rejected 422
8 Revocation runbook < 5 min to revoke, rotate, audit
9 Quarterly audit Report produced, stale keys revoked
10 Pre-commit + CI scanning Secret commit blocked locally and in CI

Follow these steps and you reduce the likelihood of a leaked key to near zero — and limit the damage if one that damage to minutes, not months.

Tagsapi-keyopenaisecurityauthentication

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All api keys & authentication for llm apis posts →