DEV Community

LachlanHolm6518
LachlanHolm6518

Posted on

Abusive Tenant API Key: How to Revoke 1 via an Admin Endpoint

Stop the abusive customer's credential at the authentication boundary, then verify that new requests produce neither billable usage nor access. For a marketplace that meters customer traffic, the least complex option is a shared, durable key record with a revoked_at field and an authenticated admin action that changes it. No deploy is needed. Short answer: reject a revoked key before recording a usage event, and retain the key's customer identity for invoice reconciliation.

Control Pick this when Blast radius Operational check
Revoke one key The abusive traffic maps to one credential One credential; other keys for that customer still work Replayed key is denied everywhere
Suspend one customer Multiple credentials for one customer are involved Every credential belonging to that customer Healthy customers still succeed
Rotate a shared secret A credential used by several customers has leaked Every holder of that secret All holders have migrated before retirement

Which boundary should carry the stop signal?

Pick key revocation when a single marketplace integration is the source of free-tier abuse and another integration for that customer should continue. Pick customer suspension when there is no trustworthy way to isolate one key, or the abuse spans several keys. A shared secret is the widest lever: rotating it affects every holder. The difference matters to invoices as much as availability. If one key can impersonate several customers, a denial cannot be attributed cleanly to one customer; issue customer-scoped credentials before relying on per-key emergency controls.

The diagram in words: admin operator -> protected control endpoint -> shared key store -> request authentication -> metering gate -> usage ledger. The admin path must never accept the tenant key as its own authorization. Keep its operator credential separate, restrict access to the control plane, and record who made the change. OWASP's secrets guidance covers access controls, rotation, and auditability; it does not make a public admin endpoint safe by itself.

How can an admin endpoint revoke an abusive tenant API key without a deploy?

Here is a small TypeScript reference implementation. The adapter contract is the important part: revoke must update one existing record durably and atomically; find must read the same authoritative state on every API instance. The in-memory adapter makes the example executable for a local drill only. Replace it with a shared database adapter before running multiple processes. Store only a digest of each generated high-entropy key, never the raw key; keep the key identifier separate so an operator can target a record without pasting the secret into an incident ticket.

import { createHash, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

type KeyRecord = { id: string; customerId: string; digest: string; revokedAt: string | null };
interface KeyStore {
  findByDigest(digest: string): Promise<KeyRecord | undefined>;
  revoke(id: string, at: string): Promise<boolean>;
}

const records = new Map<string, KeyRecord>(); // Local drill adapter; use shared durable storage in production.
const store: KeyStore = {
  async findByDigest(digest) {
    return [...records.values()].find((record) => record.digest === digest);
  },
  async revoke(id, at) {
    const record = records.get(id);
    if (!record) return false;
    if (!record.revokedAt) records.set(id, { ...record, revokedAt: at });
    return true;
  },
};
const digest = (value: string) => createHash("sha256").update(value).digest("hex");
const operatorToken = process.env.OPERATOR_TOKEN;
if (!operatorToken || operatorToken.length < 32) throw new Error("Set a strong OPERATOR_TOKEN");

function authorized(header: string | undefined): boolean {
  const supplied = header?.startsWith("Bearer ") ? header.slice(7) : "";
  const actual = Buffer.from(digest(supplied), "hex");
  const expected = Buffer.from(digest(operatorToken!), "hex");
  return timingSafeEqual(actual, expected);
}

createServer(async (req, res) => {
  const path = new URL(req.url ?? "/", "http://localhost").pathname;
  if (req.method === "POST" && /^\/admin\/keys\/[A-Za-z0-9_-]+\/revoke$/.test(path)) {
    if (!authorized(req.headers.authorization)) { res.writeHead(403).end(); return; }
    const id = path.split("/")[3];
    const found = await store.revoke(id, new Date().toISOString());
    console.info(JSON.stringify({ action: "key_revocation", keyId: id, found }));
    res.writeHead(found ? 204 : 404).end();
    return;
  }
  const rawKey = req.headers["x-api-key"];
  const key = typeof rawKey === "string" ? await store.findByDigest(digest(rawKey)) : undefined;
  if (!key || key.revokedAt) { res.writeHead(401).end(); return; }
  // Only accepted work may create a metered usage event for key.customerId.
  res.writeHead(200, { "content-type": "application/json" });
  res.end(JSON.stringify({ customerId: key.customerId }));
}).listen(3000);
Enter fullscreen mode Exit fullscreen mode

The empty map is deliberate: this is a control-flow drill, not a ready-to-deploy database. In production, provision keys through a separate issuance workflow, put the admin endpoint behind operator authentication and network policy, validate operator authorization for the specific customer, and commit revocation to shared storage before returning 204. Do not put raw keys or operator tokens in request logs. A failed store read must fail closed; it must not silently accept a key based on an old cache entry. Consider an operator who revokes key A while two API replicas process requests: if replica one reads the updated record but replica two trusts a five-minute local cache, replica two can still accept traffic and write usage events after the admin endpoint returns. That is a real consistency trade-off, not a logging problem. Require authoritative reads for this check, or design cache invalidation with a tested maximum propagation interval and an alert on post-revocation accepts. Reconcile the ledger using accepted event IDs rather than deleting all of that customer's usage; another valid key might be active at the same time.

No secret in the log.

What proves the stop worked?

Run a before/after test with two customer-scoped keys and a known usage event. Before revocation, both authenticate and accepted requests enter the ledger. Revoke key A through the admin path; replay it against each serving instance. Expect 401 and zero new accepted usage events for A. Key B must still authenticate and produce usage for its own customer. Repeat the admin request to check idempotence, then test an unknown ID and an unauthorized operator. The check is about containment, not a fast-looking 204 response.

Keep the second key live.

Instrument key_revocation audit records with operator identity, key ID, customer ID, timestamp, and outcome, but no raw credential. Count authentication denials by reason and count accepted metering events by customer; avoid putting raw keys in metric labels. Alert when a revoked key continues to produce accepted events. Watch the interval between the admin commit and the last accepted request across instances: a cached authorization decision can extend the blast radius. If requests can already be in flight when the commit lands, define the boundary explicitly as requests whose authentication starts after the commit, and reconcile earlier accepted events using their event IDs and timestamps. Never delete historic usage just because its key is now revoked.

Limits of the one-key switch

This one-key approach is not suitable when all tenants share a credential: revoking it would interrupt innocent customers too. It also cannot stop traffic authenticated through other keys held by the same abusive customer; use a customer-level suspension in that case. The example does not supply persistence, operator identity, rate limiting, or a distributed cache protocol. Those belong in the deployed control plane and its tests. A digest lookup may need an indexed key identifier or a keyed digest at scale; the linear scan shown here is only for a local drill. Keep the control path available independently of application releases, and practice a revocation across every serving instance before an incident. The decision rule stays narrow: use the smallest credential scope that stops abuse while preserving trustworthy invoice attribution.

Further reading

Top comments (0)