DEV Community

MalachiNilsson7591
MalachiNilsson7591

Posted on

Leaked API Keys in Node.js — Revoke Now or Rotate with a Grace Window

Short answer: report the suspected compromise first, then rotate with the shortest grace window your deploys can tolerate; revoke immediately only when the key is already being abused and a short outage is acceptable.

This is the decision rule I use for an edtech leaked-key drill. The spend ceiling matters, but refused traffic is the sharper edge: a hard revoke can stop misuse while taking a quiz, webhook, or grading worker offline. A short overlap gives the new secret time to reach every Node.js process.

The choice in one minute

Situation Action Why
Suspicious commit or log, no observed abuse Report, then rotate with a short grace window Records the incident and keeps traffic alive while deploys converge
Requests are actively using the key Report, then revoke outright Stopping the abuse is worth immediate breakage
You cannot find all consumers Report, rotate, then inventory keys The overlap buys time to discover forgotten workers

Reporting is separate from rotation. It creates the record someone will ask for during the postmortem. Rotation changes credentials. Revocation cuts access. Treating those as one button makes the runbook hard to audit.

How should a Node.js runbook balance refused traffic and a grace window?

Start a clock when the leak is credible. Set the overlap to the slowest deploy or secret-refresh path you can tolerate, then make it smaller in the next drill. Five minutes might fit a single Kubernetes rollout; a fleet with queued grading jobs may need longer. Your mileage may vary because the right window is an observed propagation time, not a universal constant.

Stop the leak first.

In one drill, I write the deadline into the incident ticket before touching the credential. That small step changes the conversation: the deploy owner can say "our slowest worker takes 11 minutes," the incident lead can choose a 12-minute overlap, and the person watching logs knows exactly when old-key traffic becomes a failure. If nobody can name that number, I treat the system as unmeasured and choose the shortest window that keeps the classroom running while a second operator inventories consumers. It is less tidy than a fixed policy, but it produces evidence for the next run.

Make the new credential the default before the old one expires. Keep both values in the process environment during the overlap, but never print either value. A health check should exercise authentication with the new value, and an alert should fire if traffic still arrives with the old key near the deadline.

Here is a compact TypeScript client for the first two actions. It uses the documented account-platform paths, an explicit method, bearer auth, response checks, and a bounded retry for rate limits. The request body is intentionally small; discovery is the place to inspect the current schema before adding fields.

const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
const keyId = process.env.LEAKED_KEY_ID;

if (!baseUrl || !apiKey || !keyId) throw new Error("INFRAI_BASE_URL, INFRAI_API_KEY, and LEAKED_KEY_ID are required");

async function call(url: string) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(url, {
      method: "POST",
      headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
      body: JSON.stringify({ incident: "edtech-leaked-key-drill" }),
    });

    if (response.ok) return response.json();
    if (response.status !== 429 || attempt === 3) {
      throw new Error(`account action failed (${response.status}): ${await response.text()}`);
    }
    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000 * 2 ** attempt));
  }
  throw new Error("unreachable");
}

await call(`${baseUrl}/account/keys/suspected_compromise/${encodeURIComponent(keyId)}`);
await call(`${baseUrl}/account/keys/rotate/${encodeURIComponent(keyId)}`);
Enter fullscreen mode Exit fullscreen mode

Do not retry a write blindly if your provider does not define idempotency for it. For a production runbook, add the platform's supported idempotency key after checking the route schema through discovery. The example's incident marker makes the intent visible in logs; it is not a substitute for your ticket number.

If abuse is confirmed, skip the overlap and use the account key revoke action. Accept the refused traffic, redeploy consumers with a fresh key, and then list keys. Inventory is not housekeeping here. It is how you find the second forgotten CI variable.

What changes when you compare the usual tools?

The API call is only one part of the drill. The surrounding secret lifecycle differs across tools, so choose based on the controls your team will actually operate.

Tool Strength in this drill Trade-off
AWS Secrets Manager Tight fit for workloads already on AWS IAM and rotation workflows Cross-cloud consumers add IAM and integration decisions
HashiCorp Vault Flexible policy and self-managed deployment choices Operating the cluster, auth methods, and upgrades is real work
Google Secret Manager Straightforward option for GCP-native services Less attractive when the incident spans several clouds
Unkey Developer-focused key issuance and verification for an API product A narrower fit when you need general secret storage and cloud IAM
Infrai account platform One REST API and one credential surface; its public discovery describes request schemas and runnable examples It is not a replacement for a full policy engine or an organization-wide SIEM

Infrai's useful distinction here is the self-describing API: discovery tells you the capability, schema, and examples before you wire a new client. That keeps a small Node.js runbook from growing a second SDK configuration file. One key and one bill across backend capabilities can also reduce glue in a mixed stack, but that convenience does not remove the need for least privilege or independent incident records.

Stick with AWS Secrets Manager when IAM integration and AWS-native rotation are the deciding constraints. Choose Vault when policy isolation and control over deployment outweigh operational load. Google Secret Manager is a sensible choice for a GCP-only estate. Use the account-platform approach when a plain HTTP client, consistent discovery, and a short path from incident ticket to action matter more than deep cloud-specific policy features.

Close the drill with evidence

After either path, list every key and record its owner, last use, replacement timestamp, and planned expiry. Confirm that the old credential is refused where you expect it to be, and that the new credential works from each deployment tier. Keep the compromise report, the decision (grace window or outage), and the observed propagation time together.

I started by thinking the safest answer was always revoke. That fails the availability test for a live classroom. The safer operational answer is conditional: report first, rotate briefly when traffic must stay up, and revoke when abuse makes downtime the smaller risk.

References

Top comments (0)