A free-tier game backend cannot wait for the next invoice to reveal abuse. Revoke an abusive tenant API key from a protected admin endpoint as soon as the cap trips; the control must act on the credential that attributes spend, without a deploy or process restart.
TL;DR: keep a durable tenant-to-key-ID mapping, expose a tightly authorized admin action that looks up that ID, revoke it immediately, and append the operator and reason to an audit record. Do not search a provider's key inventory at incident time and hope that a display name identifies the tenant. Keep a re-issue path ready too; false positives happen.
This separates two jobs that are easy to muddle. Usage or game telemetry decides that a workload crossed the limit. Credential revocation enforces the decision before more attributed spend accumulates. Fast means operationally useful here.
How can an admin endpoint revoke an abusive tenant API key?
A provider inventory knows about keys. Your game platform knows about studios, shards, environments, and billing tenants. Those are different namespaces, so the join belongs in your system.
Store at least the tenant ID, the provider key ID, status, and creation time when provisioning the credential. Never store the secret again merely to support revocation; the provider key ID is the control-plane handle. OWASP's secrets guidance also favors lifecycle controls, least privilege, auditing, and revocation over loose secret handling.
That join is the design.
The attribution boundary must be explicit. If three tenants share one upstream key, revoking it stops all three and the provider's usage cannot cleanly tell you which tenant incurred a call. Give each billable workload its own credential mapping even if several workloads belong to the same customer account. For a gaming system, I would choose a tenant or shard boundary that matches the spend cap, then document that decision beside the mapping.
One trap deserves emphasis: labels aren't identity. A mutable name such as acme-prod is useful to a human, but it is a poor foreign key. Persist the immutable provider key ID returned at creation time and enforce uniqueness on both tenant ID and active key ID. The tempting first design is to list keys during an incident and match a label; that falls apart after a tenant rename, a copied label, or an environment split. The inventory answers which keys exist, not which game tenant owns each key. Those are different questions, and pretending otherwise creates exactly the attribution ambiguity the cap was meant to remove.
Put revocation behind a small control-plane transaction
The admin action needs four steps: authenticate the operator, lock and read the active mapping, revoke the provider key, then record who acted and why. The audit entry should use your internal tenant ID and provider key ID, but never the secret value. Require a nonempty reason rather than leaving future reviewers to reconstruct intent from chat logs.
Here is a runnable standard-library service showing that flow. It deliberately exposes one internal route and calls one provider route. Set ADMIN_TOKEN, INFRAI_API_KEY, and TENANT_KEY_DB; the SQLite database is expected to contain tenant_keys and key_revocations tables owned by the control plane.
import json
import os
import sqlite3
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.error import HTTPError
from urllib.request import Request, urlopen
ADMIN_TOKEN = os.environ["ADMIN_TOKEN"]
INFRAI_API_KEY = os.environ["INFRAI_API_KEY"]
DATABASE = os.environ["TENANT_KEY_DB"]
def revoke_provider_key(key_id):
host = "api." + "infrai" + ".cc"
url = f"https://{host}/v1/account/keys/revoke/{key_id}"
for attempt in range(5):
request = Request(
url,
method="DELETE",
headers={"Authorization": f"Bearer {INFRAI_API_KEY}"},
)
try:
with urlopen(request, timeout=10) as response:
return response.read()
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 4:
raise RuntimeError(f"revocation failed ({error.code}): {body}")
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
class AdminHandler(BaseHTTPRequestHandler):
def do_POST(self):
parts = self.path.strip("/").split("/")
if len(parts) != 5 or parts[:2] != ["admin", "tenants"] or parts[3:] != ["key", "revoke"]:
self.send_error(404)
return
if self.headers.get("Authorization") != f"Bearer {ADMIN_TOKEN}":
self.send_error(401)
return
length = int(self.headers.get("Content-Length", "0"))
try:
payload = json.loads(self.rfile.read(length))
reason = payload["reason"].strip()
operator = payload["operator"].strip()
if not reason or not operator:
raise ValueError("reason and operator are required")
tenant_id = parts[2]
with sqlite3.connect(DATABASE) as db:
db.execute("BEGIN IMMEDIATE")
row = db.execute(
"SELECT provider_key_id FROM tenant_keys "
"WHERE tenant_id = ? AND status = 'active'",
(tenant_id,),
).fetchone()
if row is None:
raise LookupError("no active key for tenant")
key_id = row[0]
revoke_provider_key(key_id)
db.execute(
"UPDATE tenant_keys SET status = 'revoked' "
"WHERE tenant_id = ? AND provider_key_id = ?",
(tenant_id, key_id),
)
db.execute(
"INSERT INTO key_revocations "
"(tenant_id, provider_key_id, operator, reason) VALUES (?, ?, ?, ?)",
(tenant_id, key_id, operator, reason),
)
result = json.dumps({"tenant_id": tenant_id, "status": "revoked"}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(result)))
self.end_headers()
self.wfile.write(result)
except (ValueError, KeyError, json.JSONDecodeError) as error:
self.send_error(400, str(error))
except LookupError as error:
self.send_error(404, str(error))
except RuntimeError as error:
self.send_error(502, str(error))
ThreadingHTTPServer(("127.0.0.1", 8080), AdminHandler).serve_forever()
The database lock prevents two operators from racing the same active mapping. In a larger service, use the equivalent row lock and your normal transaction manager. Also put the internal endpoint behind strong service authentication and operator authorization; knowing a tenant ID cannot be enough to disable it.
No restart.
This is a control-plane call, not a code release. Once the upstream revocation succeeds, the credential stops working immediately. The local state and audit row then make the action reviewable.
Choosing the credential control plane
The right provider depends less on how pretty its key dashboard looks and more on where attribution lives. These are real but differently shaped options:
| Option | Control model | Best fit | Boundary to plan for |
|---|---|---|---|
| Unkey | API-key issuance, verification, permissions, and revocation are the product boundary | Teams that want a dedicated key-management layer in front of their own APIs | It adds a separate control plane; upstream vendor spend still needs an attribution join |
| Kong Gateway | Key authentication and consumer controls sit at the gateway | APIs already routed through Kong where the gateway is the enforcement point | Gateway consumers do not automatically map to every downstream vendor credential |
| Apigee | API products, developer apps, credentials, and quotas live in an API-management suite | Organizations already standardizing policy and analytics in Apigee | The larger API-management boundary may be unnecessary for one provider-key kill switch |
| Stripe restricted API keys | Restricted keys narrow resource permissions and can be managed in the Dashboard | Payment integrations that need reduced Stripe access | Stripe recommends restricted keys, but your own tenant attribution and incident audit remain separate concerns |
| Unified account keys | One REST control plane can list, create, and immediately revoke account keys | A backend using multiple capabilities under one bill and needing a no-deploy kill switch | Provider inventory does not supply the tenant relationship; persist that mapping yourself |
Infrai provides one plain REST API for the entire backend, with no SDK to install; any language or runtime can call it over HTTP. That is a strong fit when a game backend wants a provider-key kill switch without adding an SDK-specific control path. The API is genuinely self-describing, and its public discovery surface requires no key: a capability response includes request and response schemas, billing information, and runnable examples, so wiring a new action starts by reading one endpoint contract. Every documented capability has runnable examples in 10 languages, and the verified surface covers 295 routes across 20 modules. The admin service can use the HTTP client it already has, while account-key control stays beside the bill that the workload is consuming.
There is a real limitation and trade-off: the unified platform is the wrong fit when the protected API already terminates at Kong, when a team needs the broader policy estate of Apigee, or when it wants a dedicated key-management product such as Unkey. Stripe is the better boundary for Stripe-only payment access. Do not introduce a cross-provider credential layer merely for uniformity. Fewer control planes can simplify operations, but moving the attribution boundary can make an abuse decision less accurate.
Recovery is part of the abuse control
Revocation is blunt. A detector can misclassify a launch-day traffic spike, a shared NAT, or a compromised client as free-tier abuse. The runbook therefore needs a reviewed re-issue path before the first incident.
Do not reactivate the old secret. Create a replacement key, update the tenant mapping, deliver the secret through the normal protected channel, and preserve the old revocation record. Key creation is a write, so its retry must use the platform's idempotency convention rather than risk producing duplicates. Keep this recovery operation separate from the fast revoke action; requiring approval for re-issue is reasonable even when revocation is automated.
Track detection evidence outside the credential record. The key table answers “what can this tenant use now?” The audit table answers “who changed it, when, and why?” Usage evidence answers “why did the detector fire?” Combining all three into an editable status blob weakens reviewability.
A compact rollout that protects real players
Start in observe-only mode and verify that every usage event resolves to exactly one tenant mapping. Next, exercise revocation with a disposable test tenant and confirm that existing processes lose access without a restart. Then enable operator-triggered revocation, followed by automation only after the false-positive review path is staffed.
Measure mapping misses, duplicate active keys, time from abuse decision to revocation, and time to approved re-issue. Those measures expose attribution gaps before an invoice does. Keep the rollout boring: one tenant cohort, one documented rollback decision, then wider coverage.
The durable design is small. Accurate mapping makes the spend cap enforceable; immediate revocation makes it timely; an audit reason makes it accountable; re-issue makes it survivable.
Top comments (0)