Skip to content

Engineering blog

Why API keys fail for high-risk actions

Static API keys cannot answer which human authorized a specific irreversible run. Hardware-signed, parameter-bound approvals can — and your code can verify them offline before it executes.

For three decades, backend engineering has relied on a simple paradigm for authorization: static API keys and bearer tokens. Whether it is a cron job wiping old logs, a GitHub Action deploying to production, or a microservice initiating a database migration, we issue a long-lived secret, store it in an environment variable, and grant the service full execution rights.

That paradigm worked when automated tasks were simple and predictable. In an era of complex CI/CD automation, autonomous AI agents, and regulators asking for demonstrable human oversight, static keys have become a liability at exactly the moments that matter most.

The three fundamental flaws

  1. No out-of-band human verification. If an automated process attempts a catastrophic action — DROP DATABASE, a wire transfer, a cluster teardown — a static API key executes it instantly without asking anyone.
  2. Repudiation and identity ambiguity. When an audit log records ServiceAccount_CI_Prod, you cannot prove which human authorized that specific run. You can prove a credential was used. That is not the same claim.
  3. Malleable audit records. Traditional audit logs are rows in a centralized database. Anyone with admin access can modify or delete them after an incident, and the party attesting to the log is the party being audited.

Separating request from authorization

The fix is to stop treating the credential as the authorization. The pipeline keeps its key — it needs one to reach the gateway at all — but the key no longer carries the authority to execute. It carries the authority to *ask*.

import { IntygaClient, verifyApprovalReceipt } from "@intyga/sdk";

const intyga = new IntygaClient({
  gatewayUrl: process.env.INTYGA_GATEWAY_URL!,
  clientId: process.env.INTYGA_CLIENT_ID!,
  clientSecret: process.env.INTYGA_CLIENT_SECRET!,
});

const action = {
  target: "prod-us-east-1-controlplane",   // where this will run (Target Isolation)
  actionType: "destroy_cluster",
  params: { clusterId: "prod-us-east-1" },
};

// Blocks until a human signs with a passkey or security key, or the deadline passes.
const approval = await intyga.requireApproval("Destroy cluster prod-us-east-1", action);
if (approval.status !== "APPROVED") throw new Error(approval.status);

// Then prove it locally: re-derive the signed payload from the params about to execute,
// and check the signature against approver keys resolved from YOUR key management —
// never the key inside the receipt, which would only prove the receipt agrees with itself.
const check = verifyApprovalReceipt(approval.receipt!, {
  ...action,
  nonce: approval.nonce!,
  approvers: { dids: TRUSTED_APPROVER_DIDS, resolveKey },
}, {
  expectedOrigin: process.env.INTYGA_WEBAUTHN_ORIGIN!,
  expectedRpId: process.env.INTYGA_WEBAUTHN_RP_ID!,
});
if (!check.ok) throw new Error(`Refusing to proceed: ${check.reason}`);

Two properties are worth dwelling on, because they are what separate this from a fancy 2FA prompt.

The caller cannot name its own approval threshold

Requesters supply an action ID, which selects an exact rule. Every specific rule must retain the tenant baseline. New workspaces deny unknown IDs unless an administrator chooses baseline approval. Older workspaces retain their recorded semantics until signed activation. The executing service verifies the receipt against the operation it will perform.

Verification re-binds at execution time

You pass the parameters you are about to execute back into the verifier, which recomputes the canonical payload and compares it against what was actually signed. If the executed action drifts from the approved one by a single byte — a swapped cluster ID, an appended region — verification fails. The approval is a claim about a specific instruction, not a session.

How the signing ceremony works

  1. Challenge generation. The gateway creates a challenge cryptographically bound to the exact payload hash and a single-use nonce.
  2. Zero-install sign-off. The approver opens a browser tab. Touch ID, Windows Hello, or a YubiKey signs the hash. No proprietary mobile app, no MDM enrolment.
  3. Receipt issuance. The gateway verifies the ECDSA P-256 signature and returns a receipt your code can check itself, offline, with no secret and no callback.

Where static keys still belong

Most of your automation. Read-only queries, health checks, log shipping, metrics scraping, and the thousand routine mutations that are trivially reversible do not need a human in the loop, and putting one there just trains people to click through prompts. Reserve signed approval for actions where being wrong is expensive and undoing it is hard.

If you govern a handful of actions a day on a single platform, a well-configured GitHub Environments rule or an MFA-protected API call is probably enough. The case for a dedicated primitive starts at the seams — when the approval has to span platforms, outlive the tool that issued it, or be proved to someone who does not trust either of you.