Rotate a Leaked Production API Key Without Breaking Live Traffic
This guide is for developers who need to rotate an API key that is actively used by production traffic right now. You’ll revoke the leaked key safely, deploy the replacement with overlap where possible, and verify that live requests are using the new credential before you remove the old one.
TL;DR — If a production API key leaked, do not revoke it first unless the provider gives you a second active key or your app already supports dual-key rollout. Create a new key, deploy it everywhere that sends traffic, verify requests are succeeding with the new key, then revoke the old key and search logs and CI/CD secrets for any remaining references. Reading time: ~5 min
Goal
When you finish, production traffic is using a newly issued API key, the leaked key is revoked, deployments and background jobs no longer reference the old secret, and you have command-line proof from logs and live health checks that requests are succeeding after the cutover.
Prerequisites
- Access to the API provider account that issued the leaked key, with permission to create and revoke keys
- Access to production secret storage or runtime config (for example: Kubernetes Secret, systemd environment file, Docker Compose env file, CI/CD secret store)
- Access to deploy or restart the workloads that send the API traffic
- Access to application logs, metrics, or request traces in production
curlinstalled — check with:
curl --version
jqinstalled for JSON inspection — check with:
jq --version
- If using Kubernetes:
kubectlconfigured for the production cluster — check with:
kubectl version --client
kubectl config current-context
- If using Docker Compose on a host: shell access to the host and
docker compose— check with:
docker compose version
- The exact list of places the key is used: web app, worker, cron, serverless function, CI job, webhook forwarder, local
.envcopied to servers, etc. - The leaked key value and the variable name it is stored under, for example
THIRD_PARTY_API_KEY
Steps
Step 1: Inventory every runtime that currently uses the key
Run a repo and deployment search before changing anything.
export OLD_KEY='paste-the-leaked-key-here'
export VAR_NAME='THIRD_PARTY_API_KEY'
git grep -n "$VAR_NAME" || true
git grep -n "${OLD_KEY:0:12}" || true
If you have Kubernetes, list Secrets and workloads that reference the variable name:
kubectl get deploy,statefulset,daemonset,cronjob -A -o yaml | grep -n "$VAR_NAME" || true
kubectl get secrets -A -o yaml | grep -n "$VAR_NAME" || true
If you have env files on hosts:
sudo grep -R -n "$VAR_NAME" /etc /opt /srv 2>/dev/null || true
Success looks like a concrete list of every service, job, and secret location that must be updated.
Step 2: Create a replacement key at the provider
In your provider's dashboard, create a new key with the same scope as the old one and label it with the current date, for example prod-2026-08-10-rotation.
Exact menu path varies by provider; use your provider's dashboard path for API credentials (for example: Settings/Developers/API Keys or Security/API Credentials) and click Create key.
If the provider has a CLI, use it; otherwise use the dashboard. Record the full new key once, immediately, in your secret manager.
Success looks like a second active key visible in the provider account, or a newly generated replacement key if the provider only allows one active key.
⚠️ If the provider allows only one active key at a time, revoking or regenerating the key before you deploy the replacement will break live traffic immediately. In that case, prepare every config change first, reduce rollout time, and expect a brief error window during the cutover.
Step 3: Store the new key in your secret backend without deleting the old one yet
For Kubernetes, update the Secret value and keep the same variable name:
kubectl -n production create secret generic app-secrets \
--from-literal=THIRD_PARTY_API_KEY='paste-the-new-key-here' \
--dry-run=client -o yaml | kubectl apply -f -
For a systemd-managed app using an env file:
sudo cp /etc/myapp/myapp.env /etc/myapp/myapp.env.bak.$(date +%F-%H%M%S)
sudo sed -i "s/^THIRD_PARTY_API_KEY=.*/THIRD_PARTY_API_KEY=paste-the-new-key-here/" /etc/myapp/myapp.env
sudo grep '^THIRD_PARTY_API_KEY=' /etc/myapp/myapp.env
For Docker Compose using .env:
cp .env .env.bak.$(date +%F-%H%M%S)
sed -i "s/^THIRD_PARTY_API_KEY=.*/THIRD_PARTY_API_KEY=paste-the-new-key-here/" .env
grep '^THIRD_PARTY_API_KEY=' .env
Success looks like the new key present in the secret store or env file under the same variable name.
Step 4: Roll out the new key to every live workload
For Kubernetes Deployments and StatefulSets:
kubectl -n production rollout restart deploy
kubectl -n production rollout restart statefulset
kubectl -n production rollout status deploy --timeout=180s
kubectl -n production rollout status statefulset --timeout=180s
For Kubernetes CronJobs, update the Secret first; new Jobs will pick it up on the next run. To force a one-off validation run:
kubectl -n production create job --from=cronjob/my-cronjob manual-key-rotation-test-$(date +%s)
For systemd:
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
sudo systemctl status myapp.service --no-pager -n 20
For Docker Compose:
docker compose up -d --force-recreate
Success looks like all services restarted cleanly with no crash loop and no auth failures in startup logs.
Step 5: Verify production is actually using the new key before revoking the old one
Check app logs for auth failures first.
For Kubernetes:
kubectl -n production logs deploy/myapp --since=10m | egrep -i '401|403|unauthorized|forbidden|invalid api key|authentication failed' || true
For systemd:
journalctl -u myapp.service --since '10 minutes ago' | egrep -i '401|403|unauthorized|forbidden|invalid api key|authentication failed' || true
Then hit your app's health or a safe code path that triggers the third-party API call.
curl -fsS https://your-app.example.com/healthz
Expected success shape:
{"status":"ok"}
If you have request logs with upstream status, you want to see 2xx responses after the restart, not 401/403. Success looks like live requests succeeding for several minutes with no old-key auth errors.
Step 6: Revoke the leaked key
In the provider dashboard, open the old key and click Revoke, Disable, or Delete, depending on the provider wording.
If the provider has an API or CLI, use that exact revoke action for the old key ID. Do not revoke the new key by label similarity; verify the key ID or last 4 characters first.
Success looks like the old key status changing to revoked/disabled and no increase in 401/403 responses from your app.
Step 7: Remove residual references and invalidate caches
Search again for the old key value and variable references in deployment artifacts, CI/CD, and logs.
git grep -n "${OLD_KEY:0:12}" || true
kubectl get secrets -A -o yaml | grep -n "${OLD_KEY:0:12}" || true
sudo grep -R -n "${OLD_KEY:0:12}" /etc /opt /srv 2>/dev/null || true
If your CI/CD stores secrets separately, replace the secret there too, then trigger a fresh deploy so the next release does not reintroduce the leaked key.
Success looks like zero remaining matches for the old key in active config and a clean post-rotation deploy path.
Verify it works
Run these checks end to end.
curl -fsS https://your-app.example.com/healthz
Expected:
{"status":"ok"}
Check recent logs for auth failures after revocation:
kubectl -n production logs deploy/myapp --since=15m | egrep -i '401|403|unauthorized|forbidden|invalid api key|authentication failed' || true
Expected: no output.
If your app exposes metrics, verify the upstream dependency is still returning success. For example, your dashboard should show normal request volume and no sustained increase in 401/403 or 5xx after the revoke timestamp.
If the provider shows key usage per credential, the new key should show current traffic and the old key should show zero new requests after revocation.
Common pitfalls
Revoking the old key before any workload has the new one
Mistake: clicking Revoke first because the leak is urgent.
Symptom: immediate 401/403 responses from production, often visible within seconds in logs like:
upstream auth failed: 401 Unauthorized: invalid api key
Fix: create the new key first, deploy it everywhere, verify traffic, then revoke the old key.
Updating the secret but not restarting long-lived processes
Mistake: changing a Kubernetes Secret, env file, or CI variable and assuming running processes reload it.
Symptom: config looks updated, but logs still show auth failures using the old credential until pods or services restart.
Fix: run kubectl rollout restart ..., systemctl restart ..., or docker compose up -d --force-recreate.
Forgetting workers, CronJobs, or serverless functions
Mistake: rotating only the web app deployment.
Symptom: the website works, but background jobs start failing with 401/403 minutes later.
Fix: search all workloads for the variable name, update every runtime, and force a test run for scheduled jobs.
CI/CD reintroduces the leaked key on the next deploy
Mistake: patching production by hand but leaving the old secret in the pipeline or IaC.
Symptom: everything is fine until the next deployment, then auth failures return.
Fix: replace the secret in CI/CD and infrastructure code before the next release, then run one fresh deploy to confirm persistence.
Searching for the full key only
Mistake: searching exact full secret text when some systems mask or truncate it.
Symptom: you think the old key is gone, but a templated secret or copied env file still contains it.
Fix: search for the variable name and a stable prefix like the first 8-12 characters, then inspect each hit manually.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI