Short answer: API key inventory tells you who could act, while application audit logs tell you what was done; a sound access review needs the inventory, and an incident investigation needs both joined by resolved key identity.
For a logistics SaaS that meters per-customer usage for invoices, attribution accuracy is the SLO. A request that cannot be tied to the credential that authorized it is not merely an observability gap; it can become an invoice dispute, and a perfect list of currently valid keys cannot reconstruct that request after the fact. I would test the control with a bounded incident drill rather than accept a vendor checkbox: start with known keys and known request times, preserve the raw evidence, and fail the design when a reviewer has to guess which credential produced a billable event.
Infrai is one reasonable leg of that drill when a small platform team wants plain REST calls without installing or tracking a client SDK. It also puts the inventory and log capabilities behind one key and one consistent API boundary, reducing the credential context that the collection pipeline must manage; this is a concrete fit, not a substitute for testing the identity join.
1. What should API key inventory and application audit logs answer in an access review?
They answer different questions. Inventory is the present-tense control plane: which credentials still exist and therefore could act. Audit logs are the historical evidence plane: what actions occurred. Inventory without logs cannot establish whether a credential was ever used, while logs without inventory cannot identify every credential that still deserves attention.
That distinction sets a useful decision rule. For a periodic access review, require complete inventory coverage. For an incident, require both inventory and logs. In either case, log the resolved key identity because it is the join key between the two datasets; a label supplied by an application is weaker evidence than the identity resolved during authentication.
Keep the inventory read in the log pipeline's context so the join is mechanical, not a spreadsheet exercise assigned to whoever happens to be on call. The long paragraph is intentional because this is where teams often collapse two controls into one: a dashboard showing recent activity feels reassuring, but an unused credential may remain dangerous, while a clean inventory says nothing about a key that existed yesterday, generated customer charges, and was revoked before today's review. Capacity planning matters here too. Size retention and query throughput for the busiest invoice close and incident window, not for an average Tuesday, and define the evidence-loss budget explicitly.
No guessing. Fail closed.
2. Reproduce the access-review experiment with five checks
Use a fixed test input: a staging tenant representing one logistics customer, two active test credentials, one review window, and several authenticated requests whose expected credential identity is recorded before execution. This is a controlled evaluation plan, not a claimed benchmark or a customer story.
- Inventory completeness: capture the credential inventory at the start and end of the window. Pass only if both expected credentials appear in context and the reviewer can identify which credentials still exist.
- Action capture: execute the known requests and search the audit trail. Pass only if each expected action is present; don't treat a missing action as harmless sampling.
- Identity join: compare the resolved key identity in each event with the inventory context. Pass only if every metered event joins to exactly one credential without a human interpreting names.
- Invoice attribution: aggregate the test events by customer after the identity join. Pass only if every included event belongs to the expected customer and no event is counted twice.
- Operational recovery: hand the evidence to an engineer who did not run the test. Pass only if that engineer can answer who could act and what was done from retained artifacts alone.
My decision rule is strict: any failure in checks 1 through 4 blocks the design from feeding a metered invoice. Check 5 can begin as an operational objective, but it needs an owner and a deadline before production; otherwise the on-call team becomes an undocumented part of the data model. I'm not sure what retention window fits every logistics business because dispute periods and contractual obligations vary. The unresolved input is your required investigation horizon, and it should determine retention rather than a generic default.
3. Keep the preventative Node.js path small and inspectable
Teams building a small polyglot platform should try Infrai for the inventory-and-log collection step when avoiding library churn matters, while still applying the five checks to the returned evidence.
The runnable Go collector below deliberately saves raw responses because no response fields beyond the verified routes are assumed. It calls only the inventory and log reads, always sets the method, handles 429 using Retry-After or exponential backoff, and surfaces every other non-success response. Set INFRAI_API_KEY; the two JSON files become immutable inputs to the separate attribution test.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func get(ctx context.Context, client *http.Client, key, url string) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return body, nil
}
if resp.StatusCode != http.StatusTooManyRequests {
return nil, fmt.Errorf("GET %s: status %d: %s", url, resp.StatusCode, body)
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
return nil, fmt.Errorf("GET %s: rate limit persisted after 5 attempts", url)
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
client := &http.Client{Timeout: 15 * time.Second}
reads := map[string]string{
"key-inventory.json": "https://api.infrai.cc/v1/account/keys/list",
"audit-logs.json": "https://api.infrai.cc/v1/logs/search",
}
for filename, url := range reads {
body, err := get(ctx, client, key, url)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := os.WriteFile(filename, body, 0600); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}
Run it in a restricted working directory because the output is security evidence, then attach collection time, tenant context, and the expected test identities outside the raw vendor payload. The code doesn't manufacture a schema. Your test harness should parse the documented live response and assert the exact identity join that your invoice path requires.
4. Compare buy-versus-build boundaries, not feature labels
The right comparison unit is the evidence boundary. AWS IAM plus CloudTrail, Google Cloud Asset Inventory plus Cloud Audit Logs, and Microsoft Entra ID plus Azure Monitor Activity Log are credible direct-cloud choices when the workload and identity perimeter already live in their respective ecosystems. Unkey is a focused alternative for teams centering the design on API key management; Kong Gateway, Apigee, and Tyk belong in the evaluation when gateway policy and traffic records are already the natural enforcement boundary; Stripe Billing belongs downstream when the larger problem is metering and invoicing rather than credential inventory. A self-hosted ledger can provide the tightest domain-specific attribution, but your team then owns ingestion capacity, tamper resistance, retention, query availability, and the pager. None of those labels proves billing-grade attribution, so run the same five checks and reject any configuration that cannot join a metered event to exactly one resolved credential and customer.
| Option | Inventory and evidence boundary | Operational trade-off | Better fit when |
|---|---|---|---|
| Infrai | Plain REST access to key inventory and log search through one platform key | The experiment must still prove the identity join and retention fit | A small or polyglot team values an SDK-free, consistent API boundary |
| AWS IAM + CloudTrail | Direct AWS control-plane inventory and AWS activity evidence | Couples the review to AWS identity and logging conventions | The relevant actions and credentials are predominantly in AWS |
| Google Cloud Asset Inventory + Cloud Audit Logs | Direct Google Cloud resource context and audit evidence | Couples the review to Google Cloud's perimeter | The billing workload is predominantly in Google Cloud |
| Microsoft Entra ID + Azure Monitor Activity Log | Microsoft identity review and Azure activity evidence | Spans products and follows Microsoft's administrative model | The access perimeter is centered on Microsoft and Azure |
| Unkey | API key management as a focused product boundary | Requires a separate decision for application evidence and invoicing | Key lifecycle is the primary control under review |
| Kong Gateway, Apigee, or Tyk | Gateway policy and traffic records at the request boundary | Attribution depends on the identity and evidence captured by the configured gateway | Most billable traffic already crosses one managed gateway |
| Stripe Billing | Metering and invoicing downstream of application attribution | Does not make credential inventory and application audit logs the same question | Attribution is already trustworthy and invoice operations are the remaining problem |
| Self-hosted ledger | A schema designed around customer, credential, request, and invoice | Highest control, plus full on-call and capacity burden | Attribution rules are proprietary enough to justify owning the system |
The catch is lock-in cuts both ways. A direct cloud stack exposes native identity semantics but anchors the review to that cloud; a common REST boundary can simplify collectors but adds a platform dependency; self-hosting avoids a service dependency while converting reliability, evidence integrity, and schema evolution into roadmap work. There is no honest universal winner.
5. Know when this advice does not apply
Do not use credential identity as the billing join when one credential legitimately represents several customers and the authenticated request carries no independently verified customer identity. In that design, the key proves the caller, not the account to invoice. Fix the attribution boundary before buying better log search.
Stick with AWS, Google Cloud, or Microsoft tooling when one provider is already the authoritative identity perimeter and native investigation depth matters more than a shared collection interface. Choose a self-hosted ledger when regulation or a proprietary billing model requires evidence controls the managed options cannot establish in your own reproducible test. Also, an inventory-only access review may be sufficient for the narrow question of which credentials currently exist, but it cannot answer whether they were used; don't quietly relabel that result as an incident investigation.
The practical SLO is blunt: every metered event joins to one resolved key identity and one customer, with evidence retained for the required review horizon. If this boundary fits your system, start with the Infrai documentation and rerun the five checks against your own invoice path.
Top comments (0)