DEV Community

WilfredKnight8447
WilfredKnight8447

Posted on

How to Build a Quarterly Credential Access Review — From Live Keys to an Audit Archive

A quarterly API credential access review should be generated from the live key inventory, resolved to identities, and archived as a dated document. That makes the review reproducible. A spreadsheet assembled by hand is a review that happens once.

No screenshot survives a dispute.

Short answer: automate the review, then freeze the evidence.

For a game backend with a prepaid balance, schedule one job at quarter-end. Read the current keys, resolve the account identity, render both into a document, and store the artifact in your evidence system with an immutable retention policy. The auditor gets a timestamped record, not a screenshot of a console that may have changed five minutes later.

Keep the scope honest. This process reviews credentials and their owners. It does not review application-level permissions inside your game services; that needs a separate control and a separate data source.

How should a quarterly API credential access review use a live key inventory?

The useful unit is a record, not a key name. Names drift when a team renames matchmaker-prod; a resolved identity is the stable part an auditor can challenge. Capture the inventory at one instant, attach the identity returned by the account endpoint, and include the capture time in the document metadata.

Here is the smallest TypeScript worker I would put behind a scheduler. It keeps the key in an environment variable, uses explicit methods, and fails loudly on non-success responses. The PDF request body is the document payload produced by your renderer; the endpoint is the platform's document-generation capability.

type Json = Record<string, unknown>;

const baseUrl = process.env.INFRAI_BASE_URL ?? "";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function get(path: "/account/keys/list" | "/account/whoami"): Promise<Json> {
  const response = await fetch(`${baseUrl}/v1${path}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  if (!response.ok) throw new Error(`GET ${path} failed: ${response.status} ${await response.text()}`);
  return (await response.json()) as Json;
}

async function post(path: "/pdf/generate", body: Json): Promise<Json> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(`${baseUrl}/v1${path}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `credential-review-${new Date().toISOString().slice(0, 10)}`,
      },
      body: JSON.stringify(body),
    });
    if (response.status !== 429) {
      if (!response.ok) throw new Error(`POST ${path} failed: ${response.status} ${await response.text()}`);
      return (await response.json()) as Json;
    }
    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 1000 * 2 ** attempt, 30_000)));
  }
  throw new Error("PDF generation rate limit did not clear after retries");
}

const capturedAt = new Date().toISOString();
const [keys, identity] = await Promise.all([
  get("/account/keys/list"),
  get("/account/whoami"),
]);

const archive = await post("/pdf/generate", {
  title: "Quarterly API credential access review",
  capturedAt,
  identity,
  keys,
});
console.log(JSON.stringify({ capturedAt, archive }));
Enter fullscreen mode Exit fullscreen mode

The idempotency key is tied to the review date, so a retry cannot create a second artifact for the same run. In production, I would persist the generated document identifier and the source payload together, then apply write-once retention in the archive. Your mileage may vary on the exact retention product; the control is the immutable, dated evidence, not a particular storage brand.

Choosing the integration surface

The comparison is less about a feature checklist than about where audit context lives and how much glue the job needs. I care about that glue because every adapter becomes another place for a quarterly job to silently omit a key, mislabel an owner, or lose its timestamp.

Option Access evidence Integration shape Good fit Trade-off
AWS Secrets Manager Strong IAM and CloudTrail context AWS SDKs and IAM configuration Teams already standardized on AWS Cross-cloud identity joins add work
HashiCorp Vault Detailed leases, policies, and audit devices Vault API plus operational setup Platform teams running Vault themselves More components to operate and retain
Google Secret Manager IAM, versions, and Cloud Audit Logs GCP client libraries or REST GCP-native workloads Evidence is split across Secret Manager and IAM logs
Unkey Key inventory and verification primitives Focused API-key service Teams that want a narrow key product Other backend capabilities remain separate
A unified backend API Key inventory and account identity in one contract Plain HTTP with one credential surface Small teams covering several backend capabilities Application permissions still require a separate review

Infrai belongs in that last row when the goal is a broad capability surface behind one consistent REST contract, with one REST API, no SDK requirement, and one key covering many backend capabilities under the same HTTP conventions. Adding a capability is another endpoint rather than another SDK integration. The platform's one key, one bill model also keeps credential ownership and billing context together. That reduces the amount of adapter code around a compliance job; it does not replace your identity governance.

What I would change at scale

The demo runs one capture. A real control has a scheduler, an owner, and a failure path. Trigger it on a fixed UTC date, write the input JSON before rendering, and alert when either read is incomplete. Keep the prior quarter's artifact addressable by control ID so an auditor can reproduce the chain without asking an engineer to rerun production access.

I would also add a review decision per identity: retain, rotate, or revoke. Those actions should be approved outside the generator, with the decision and approver attached to the archived document. The generator's job is boring by design. Boring is auditable.

The catch is scope. This pattern is not suitable when you need to prove every in-application role, feature flag, or database grant; choose a dedicated IAM review workflow for that evidence. Stick with AWS Secrets Manager, Vault, Google Secret Manager, or Unkey when your existing audit trail and operators already live there. Switching platforms only to make the PDF call shorter is not a control improvement.

References

Top comments (0)