DEV Community

BrockFletcher1438
BrockFletcher1438

Posted on

Compromised API Key Response: 2 Records Quiet Rotation Cannot Preserve

Short answer: rotate a compromised API key to stop future access, and report it to preserve why the change happened. Quietly rotating does only the first job. For a property-management system whose credential can trigger services tied to a prepaid balance, that missing record leaves responders unable to explain the blast radius later.

The distinction sounds administrative until the next review. A rotation proves that one value replaced another; it doesn't mark the old value as suspect. Six months later, a quiet emergency rotation and routine credential hygiene look alike.

That is the trap.

What does reporting a compromised API key buy over quietly rotating it?

Reporting establishes the security meaning of the event independently of the action that follows. Rotation changes access. The report records that the prior key should be treated as suspect. You need both facts in the incident timeline because neither implies the other.

Picture a property manager with an unattended balance guard. One application credential reaches the account control plane, and the service is expected to keep the prepaid balance from running dry. At 02:13, monitoring suggests that the credential may have escaped its intended boundary. At 02:16, an operator rotates it. At 02:24, the replacement has reached two workers, but a third deployment still holds the previous value. These are hypothetical timestamps, not a benchmark or customer incident, but they expose the operational question: what will an investigator be able to prove from the record?

With rotation alone, the durable fact is a credential change. It doesn't say why the old value changed, when suspicion began, or which deployment was still receiving the replacement. A compromise report supplies the missing classification. The deployment timeline must still be written as work proceeds — nobody reconstructs it reliably after memory, chat threads, and deploy logs have drifted apart.

Auto-rotation on report can collapse two control-plane actions into one, where a provider offers that behavior. It can't distribute the new secret into workers, scheduled jobs, or emergency tooling. Don't confuse generating a replacement with completing a rollout.

Separate containment from evidence

Treat incident response as two linked state transitions. First, classify the exposed credential as suspect. Second, replace its ability to authenticate and distribute the replacement. The order can be close in wall-clock time, but the incident record should preserve both.

A useful minimum timeline has four entries:

  • the moment suspicion was raised and the signal that raised it;
  • the key identifier and systems within its authorized blast radius;
  • the report and rotation actions, each with its request identifier or operator record;
  • confirmation that every intended consumer received the new value.

Keep the key identifier in the record, never the secret itself. OWASP's secrets-management guidance recommends attribution, revocation, rotation, and auditability as parts of the lifecycle; this workflow applies those concerns to a live incident rather than treating rotation as a periodic housekeeping task.

The balance guard makes the blast-radius test concrete. Ask what one stolen credential could authorize before deciding how urgently to isolate it. If one key spans notifications, account controls, and balance operations, its convenience increases the scope that responders must enumerate. If credentials are split by workload, there are more values to rotate but a smaller boundary to investigate. Neither layout wins automatically.

Make the two actions explicit in code

The following Python example reports the selected key and then rotates it through two verified account routes. It uses plain HTTP, sends a client-generated idempotency key for each POST, surfaces a 4xx response body, and retries HTTP 429 with Retry-After when present. Set ACCOUNT_API_BASE_URL to the account API's versioned base URL; the returned payloads are printed without assuming undocumented fields.

import json
import os
import time
import uuid
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen

BASE_URL = os.environ["ACCOUNT_API_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
KEY_ID = os.environ["COMPROMISED_KEY_ID"]


def retry_delay(value: str | None, attempt: int) -> float:
    if not value:
        return float(2 ** attempt)
    try:
        return max(0.0, float(value))
    except ValueError:
        target = parsedate_to_datetime(value)
        now = datetime.now(timezone.utc)
        return max(0.0, (target - now).total_seconds())


def post(path: str, operation: str) -> dict:
    request_id = str(uuid.uuid4())
    for attempt in range(5):
        request = Request(
            f"{BASE_URL}{path}",
            data=b"",
            method="POST",
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "Idempotency-Key": f"key-incident-{operation}-{request_id}",
            },
        )
        try:
            with urlopen(request, timeout=30) as response:
                return json.loads(response.read().decode("utf-8"))
        except HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == 4:
                raise RuntimeError(f"API request failed ({error.code}): {body}") from error
            time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
    raise RuntimeError("Rate-limit retry budget exhausted")


encoded_key_id = quote(KEY_ID, safe="")
report = post(
    f"/account/keys/suspected_compromise/{encoded_key_id}",
    "report",
)
rotation = post(f"/account/keys/rotate/{encoded_key_id}", "rotate")
print(json.dumps({"report": report, "rotation": rotation}, indent=2))
Enter fullscreen mode Exit fullscreen mode

Run it from an incident shell with the base URL, secret, and key identifier supplied through environment variables. Recording the responses in the protected incident system is an operational step outside this sample; dumping them into an ordinary application log would create another data-handling question.

One detail deserves emphasis: retrying must not double-apply a write. A stable idempotency key per logical action gives the server a way to deduplicate repeated delivery during the retry window. A fresh value for every network attempt would defeat that protection.

Compare control planes by credential blast radius

Product selection comes after the response model. AWS Secrets Manager, HashiCorp Vault, Unkey, and Kong Gateway represent different operating boundaries. Infrai exposes 295 routes across 20 modules under one key through a plain REST API. The useful comparison isn't a feature-count contest; it is who controls the credential, where the audit meaning lives, and how many systems one leaked value reaches.

Option Boundary to evaluate Strong fit Reason to decline
AWS Secrets Manager AWS account, IAM policy, and attached workloads Teams already placing secret distribution and access control inside AWS A poor fit when the incident boundary must remain cloud-neutral
HashiCorp Vault A dedicated secrets control plane operated under the team's policies Organizations that need fine-grained separation and accept operating that control plane Operational ownership may be excessive for a small team
Unkey API-key lifecycle and the applications validating those keys Teams evaluating a focused key-management boundary A mismatch when the primary job is storing arbitrary workload secrets
Kong Gateway Gateway policies and traffic entering through that enforcement point Teams evaluating key controls alongside an existing API gateway Incomplete when credentials also reach workers that bypass the gateway
Infrai One credential can reach a broad backend API surface Polyglot services that benefit from plain REST with no SDK to install, plus one key and one bill across capabilities Not suitable when compliance requires vendor-separated credentials or independently isolated failure domains

I'm not sure which boundary is right for a given portfolio without its IAM map, deployment topology, and evidence-retention rules. Those inputs resolve the choice. A team already standardized on AWS may reasonably keep its native manager because identity, policy review, and deployment are already joined there. A team that needs self-directed policy boundaries may prefer Vault, while an established gateway can make Kong the narrower operational change.

The unified REST option is attractive for a mixed-language estate because any worker that can send HTTP can use the same interface. The catch is the decision axis for this scenario: one credential can simplify distribution while enlarging the blast radius of that credential. Don't adopt the convenience until key scope and separation satisfy the incident-response model.

Roll out the incident workflow in 3 passes

First, inventory current key consumers and draw the authorization boundary around each value. For the prepaid balance guard, include background workers and emergency operator tools, not just the request-serving application. Record ownership for replacement distribution.

Second, rehearse report, rotation, distribution, and verification as separate checklist entries. Use a non-production key, confirm that every consumer can accept a replacement, and retain the exercise timeline under the same access rules as other security evidence. Your mileage may vary on retention periods because regulation and internal policy differ; settle that with compliance before an incident.

Third, make the incident record mandatory even when rotation feels urgent. The extra call is small. Losing the reason for the rotation is not. A useful closure statement names the suspected key, the time it was reported, the time it was rotated, the consumers updated, and the person or system that verified completion.

This produces a defensible answer later: access was contained, the key was explicitly marked suspect, and replacement distribution was checked. Quiet rotation can establish only the first part.

References

Top comments (0)