How to automatically revoke an abused API key

Which providers revoke leaked keys for you, and how to build a kill switch on Google Cloud, OpenAI and Anthropic that disables the one key whose usage spikes.

· 7 min read

  • api keys
  • automation
  • google cloud
  • openai
  • anthropic
On this page

A stolen key costs money for as long as it keeps working. A person has to notice, find the key, log in and revoke it, and at night or on a weekend that takes hours. A script can do it in minutes.

This guide covers what the providers already do on their own, where that stops, and how to build the rest: watch each key's usage and switch off the one that spikes.

What the providers already do

GitHub runs a secret scanning partner program. When someone pushes a key to a public repository, GitHub sends it to the provider, and the provider decides what to do with it.

Provider What happens when a key is pushed to public GitHub
Anthropic The key is deactivated automatically and you get an email (Anthropic)
OpenAI GitHub forwards the key, and when the integration launched GitHub said partners "automatically disable them and notify their owners" (GitHub, 2021)
AWS A quarantine policy, AWSCompromisedKeyQuarantineV3, is attached to the IAM user, and a support case opens. Palo Alto's Unit 42 saw it attached seconds after a push
Google Cloud Service account keys, and API keys bound to a service account, are disabled by default (org policy)
Gemini API keys GitHub alerts you, the repository owner. The key isn't sent to Google (supported patterns)

This is worth having, and it's free. It also only covers one way keys leak. It doesn't catch a key in a web page, a mobile app, a Docker image, a log file or a private repo that someone cloned, and it does nothing about a key that wasn't leaked at all but is being hammered by your own runaway code. For those, the signal has to come from usage.

What "revoke" can mean

Each provider gives you different options, and some can be undone:

Provider Options Can it be undone?
Google Cloud API key Change its API restrictions, or delete it A deleted key can be restored for 30 days
Anthropic Set the key's status to inactive Yes, set it back to active
OpenAI Delete the key No, you issue a new one
AWS access key Deactivate it Yes, reactivate it

When a script makes the call, prefer the options you can undo. A false alarm then costs a few minutes, not a redeploy.

Google Cloud: alert on requests, then delete the key

Google doesn't have a built-in "disable this key when it spikes" switch. You can build one from three parts: an alerting policy on each key's request count, a Pub/Sub topic, and a small function that deletes the key.

1. Alert on each key's request count

  1. In the console, open Monitoring > Alerting > Create policy.
  2. For the metric, choose the resource Consumed API and the metric Request count (serviceruntime.googleapis.com/api/request_count).
  3. Filter service to the API you want to protect, for example generativelanguage.googleapis.com for Gemini.
  4. Group the time series by credential_id, so each key is checked on its own.
  5. Set the threshold well above each key's normal peak.

credential_id belongs to the Consumed API resource, and Google describes it as "the client credential ID, such as an API key ID" (monitored resources).

Know the lag before you rely on this. Google samples this metric every 60 seconds, and a sample can take up to 30 minutes to become visible (metric details). Even at its slowest, that's far ahead of billing data, which takes hours.

2. Send the alert to Pub/Sub

Create a topic, for example key-alerts. In Monitoring > Alerting > Edit notification channels, add a Pub/Sub channel for that topic. The first time, Monitoring creates a service account named service-PROJECT_NUMBER@gcp-sa-monitoring-notification.iam.gserviceaccount.com. Give it the Pub/Sub Publisher role on the topic (notification options). Then add the channel to the policy.

3. A function that deletes the key

The alert arrives as JSON. Its incident.resource.labels hold the labels of the time series that crossed the threshold, including credential_id. The function lists the project's keys, finds the one that matches, and deletes it.

python
# main.py
import base64
import json
import os

import functions_framework
import google.auth
from google.auth.transport.requests import AuthorizedSession

PROJECT = os.environ["PROJECT_ID"]
DRY_RUN = os.environ.get("DRY_RUN", "true") == "true"
# Keys the function must never touch, by display name.
PROTECTED = set(filter(None, os.environ.get("PROTECTED_KEYS", "").split(",")))

credentials, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
session = AuthorizedSession(credentials)
API = "https://apikeys.googleapis.com/v2"


@functions_framework.cloud_event
def on_alert(event):
    alert = json.loads(base64.b64decode(event.data["message"]["data"]))
    incident = alert["incident"]
    if incident.get("state") != "open":
        return
    credential = incident.get("resource", {}).get("labels", {}).get("credential_id", "")
    if not credential:
        print("No credential_id in the alert. Check the policy groups by credential_id.")
        return

    keys = session.get(f"{API}/projects/{PROJECT}/locations/global/keys", params={"pageSize": 300}).json().get("keys", [])
    matches = [k for k in keys if k["uid"] in credential]
    if len(matches) != 1:
        print(f"{credential} matched {len(matches)} keys. Not acting.")
        return

    key = matches[0]
    if key.get("displayName") in PROTECTED:
        print(f"{key['displayName']} is protected. Alert only.")
        return
    if DRY_RUN:
        print(f"Dry run: would delete {key['displayName']} ({key['name']})")
        return

    session.delete(f"{API}/{key['name']}").raise_for_status()
    print(f"Deleted {key['displayName']}. Undo within 30 days: gcloud services api-keys undelete {key['name']}")
text
# requirements.txt
functions-framework
google-auth
requests

Google's docs don't show what a credential_id value looks like, so the function matches on the key's uid appearing in it, and does nothing unless exactly one key matches. Before you turn dry run off, look at a real credential_id in Metrics explorer and check it contains the uid that gcloud services api-keys list --format=json shows for that key.

Deploy it with its own service account:

bash
gcloud functions deploy revoke-abused-key --gen2 --region=us-central1 \
  --runtime=python312 --entry-point=on_alert --trigger-topic=key-alerts \
  --service-account=key-revoker@PROJECT_ID.iam.gserviceaccount.com \
  --set-env-vars=PROJECT_ID=PROJECT_ID,DRY_RUN=true,PROTECTED_KEYS=maps-web-prod

4. Give it only the permissions it needs

The predefined API Keys Admin role can also create keys and read every key's secret string. The function needs two permissions, so make a custom role with just those (API Keys access control):

bash
gcloud iam roles create keyRevoker --project=PROJECT_ID \
  --title="Key revoker" --permissions=apikeys.keys.list,apikeys.keys.delete

gcloud projects add-iam-policy-binding PROJECT_ID \
  --member=serviceAccount:key-revoker@PROJECT_ID.iam.gserviceaccount.com \
  --role=projects/PROJECT_ID/roles/keyRevoker

Why not cut off billing instead?

Google has an example that removes billing from a project when a budget is exceeded. It works off billing data, which is hours late, and it stops everything in the project. Google's own warnings: it "removes Cloud Billing from your project, shutting down all resources", and "Resources might be irretrievably deleted."

That's a last resort, not a kill switch for one key. For the Gemini API specifically, a Cloud Billing spend cap budget now pauses just that API in one project, without touching anything else (spend cap budgets). Billing spike alerts and spend caps covers how to set one up.

OpenAI: poll usage per key, delete the outlier

OpenAI's Admin API reports usage per key and can delete keys. It has no disable, so deleting is the only switch (OpenAI admin APIs). It needs an admin key, which you create in the organization settings and which only works on the admin endpoints.

bash
# Requests and tokens per key, in one-minute buckets, for the last hour.
curl "https://api.openai.com/v1/organization/usage/completions?start_time=$(($(date +%s) - 3600))&bucket_width=1m&group_by=api_key_id" \
  -H "Authorization: Bearer $OPENAI_ADMIN_KEY"

# Delete one key from a project.
curl -X DELETE "https://api.openai.com/v1/organization/projects/PROJECT_ID/api_keys/KEY_ID" \
  -H "Authorization: Bearer $OPENAI_ADMIN_KEY"

Run the first call every minute or two from a scheduler, compare each key's tokens with its usual numbers, and call the second for a key that's far over. Because a deleted key is gone for good, have the script delete only on a big, sustained jump, and alert a person for anything smaller.

Anthropic: poll usage per key, deactivate the outlier

Anthropic's Admin API can set a key to inactive and back, which makes it the friendliest of the three to automate (Admin API). Admin keys start with sk-ant-admin and only organization admins can create them.

bash
# Usage per key in one-minute buckets.
curl "https://api.anthropic.com/v1/organizations/usage_report/messages?starting_at=$(date -u -d '-1 hour' +%Y-%m-%dT%H:%M:%SZ)&bucket_width=1m&group_by[]=api_key_id" \
  -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \
  -H "anthropic-version: 2023-06-01"

# Deactivate one key. Send "active" to undo.
curl "https://api.anthropic.com/v1/organizations/api_keys/KEY_ID" \
  -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"status": "inactive"}'

Anthropic says usage "typically appears within 5 minutes" and supports polling once a minute (usage and cost API). That makes a minutes-scale kill switch realistic.

Rules for any automatic kill switch

  • Start in dry run. Log what it would have done for a week, and check each decision against what you know happened.
  • Act on one key, never the project. The point is that everything else keeps working.
  • Prefer what you can undo. Restrict or deactivate before you delete, where the provider allows it.
  • Compare each key with its own history. A fixed threshold either fires on your busiest day or misses a slow, steady theft.
  • Protect the keys you can't lose, like the one your checkout uses. Alert a person for those instead.
  • Rate-limit the switch itself. A bug that matches every key shouldn't be able to switch off all of them in a minute.
  • Tell someone every time it acts, with the key, the numbers and how to undo it.

The provider's own spend controls go underneath all of this, as the last line. Billing spike alerts and spend caps covers those, and the first hour after a key leaks covers what to do once a key is switched off.

How Keyll does it

Keyll is this kill switch, built with the rules above. It watches every key's usage each minute against up to four weeks of that key's own history. At eight times its usual peak, or on a model or service the key never used, it alerts you by email and Slack. After 10 minutes of sustained abuse and a warning you can cancel, it blocks that one key:

  • On Google Cloud it changes the key's restrictions, and one click puts back the exact restrictions it had.
  • On Anthropic and OpenRouter it switches the key off, and one click switches it back on.
  • On OpenAI and Resend it alerts, and blocks only if you opt in. OpenAI's API doesn't say what a key is allowed to do, and Resend can only delete a key.

It starts in dry run and blocks at most three keys an hour. Keyll hasn't launched yet. Join the waitlist to get one email when it does.

All guides