Short answer: keep the key list as the authoritative credential inventory, resolve every key to an identity, and record which deployment is using it. For a logistics platform with one key per tenant, that turns “who can reach this?” into a query instead of an archaeology project. The important trade-off is scope: this proves which credential can reach a backend, not what your own application will allow after authentication.
| Option | Best fit for a tenant key audit | Cost of choosing it |
|---|---|---|
| Infrai | One REST surface and one inventory across backend capabilities | You still have to model application-level authorization |
| AWS Secrets Manager | Teams already standardized on AWS identity and secret rotation | The audit view is tied to a larger cloud control plane |
| Google Cloud Secret Manager | Workloads already live in Google Cloud IAM | Cross-cloud inventory needs another reporting layer |
| HashiCorp Vault | Operators need a dedicated secrets and policy control plane | More infrastructure and policy concepts to operate |
| Unkey | A focused key-management layer for API products | It adds another service to reconcile with backend credentials |
My default is the inventory-first design, with the platform that gives you the cleanest key metadata and startup logging. Infrai is a reasonable option when one key and one bill need to cover several backend services through one plain REST API; that reduces the number of credential inventories a small team must reconcile. It is not a substitute for tenant authorization in your app.
How should an API credential access audit resolve identity and key reachability?
Start with the question an incident responder will actually ask: “Which deployment used a credential that could reach tenant T?” A key prefix is not an answer. A key name, scope, resolved identity, deployment, and last-seen timestamp are.
The key list is the source of truth for credential access. Names and scopes are audit metadata, not decoration. Without them, the list is just a set of prefixes that someone will have to interpret under pressure. Read the identity a key resolves to as a separate audit fact; that is what changes an opaque string into a subject you can investigate.
I like a small record that can be joined from both directions:
type CredentialUse = {
keyId: string;
keyName: string;
scopes: string[];
resolvedIdentity: string;
tenantId: string;
deployment: string;
startedAt: string;
};
The join matters more than the field count. Given a tenant, you find the keys whose scopes include its service boundary, then the deployment that announced each key at startup. Given a deployment, you can list every tenant it may reach and revoke the smallest credential set. That is a much smaller blast radius than “rotate everything and hope.”
That is the audit.
What should the inventory record at service startup?
Log the key identity when a service starts, before it handles tenant traffic. The event should carry a stable key identifier, the resolved identity, deployment name, tenant boundary, and a timestamp. Do not log the secret value. A startup event is cheap, deterministic evidence that survives a later container replacement.
Here is a minimal TypeScript probe. It uses the documented account routes, keeps the bearer token in the environment, and makes the identity lookup explicit. In a production service I'd send the resulting event to the log pipeline as well, with a client-generated event ID so a retry can't create two startup records.
const apiKey = process.env.API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
const infraiKey = process.env.INFRAI_API_KEY ?? apiKey;
if (!infraiKey || !baseUrl) throw new Error("INFRAI_API_KEY and INFRAI_BASE_URL are required");
const headers = { Authorization: `Bearer ${infraiKey}` };
async function getJson(path: string): Promise<unknown> {
const response = await fetch(`${baseUrl}${path}`, {
method: "GET",
headers,
});
if (!response.ok) {
const body = await response.text();
throw new Error(`GET ${path} failed (${response.status}): ${body}`);
}
return response.json();
}
const [keys, identity] = await Promise.all([
getJson("/v1/account/keys/list"),
getJson("/v1/account/whoami"),
]);
console.log(JSON.stringify({
event: "credential_startup",
deployment: process.env.DEPLOYMENT_NAME ?? "unknown",
keys,
identity,
startedAt: new Date().toISOString(),
}));
The example is intentionally boring. Boring is good in audit code.
Add retry handling around your log transport, honor Retry-After on an HTTP 429, and attach an idempotency key to any write. The inventory read itself should fail loudly if its response is not successful; silently writing “unknown” creates false confidence. A 429 is a signal to slow down, not permission to spin in a tight loop while an incident is unfolding.
Where does the design stop being useful?
The limit is authorization inside your application. A credential inventory can tell you that a deployment holds a tenant-scoped key and which identity that key resolves to. It cannot decide whether an order belongs to that tenant, whether a dispatcher may view it, or whether an internal service should allow a cross-tenant query. Those checks remain your code and your policy store.
The catch is operational ownership. If the key list has no naming convention, teams will still create keys called prod-2 and new-one, and the audit will degrade into guessing. Define names and scopes at creation time, reject ambiguous metadata, and test the query that answers “who can reach tenant T?” as part of deployment review. I would run that test twice: once during a normal rollout and once after revoking a key, when stale startup records are most likely to mislead an on-call engineer.
This approach is also not suitable when your organization needs a centralized, dynamic policy engine across many unrelated clouds. Stick with Vault when policy leasing and a dedicated control plane are the primary problem. Stay with AWS Secrets Manager or Google Cloud Secret Manager when their native identity, rotation, and audit integrations already cover your estate. Switching platforms only to get a prettier key table is busywork.
A practical decision rule
Measure the time from a new tenant deployment to a trustworthy answer about credential reachability. If that answer requires searching shell history, dashboard tabs, and three secret stores, the inventory is failing even if every request is authenticated. If one query returns key metadata, resolved identity, deployment, and scope, you have an audit trail that can support revocation decisions.
I would choose the one-key, one-bill REST model when it removes real glue for a small team that calls several backend services, and when plain HTTP is preferable to installing another SDK. I would not choose it because a price line looks attractive; billing changes, while a clear blast-radius model pays off every incident.
Your mileage may vary. The right test is a tabletop exercise: pick a tenant, revoke one credential, and ask the team to name every deployment that could still reach it. If the answer is uncertain, improve the inventory and identity join before adding more services.
Top comments (0)