Operations recipes
Recipes for verifying and archiving the audit chain, stateless assertions for downstream services, observing latency, and filtering and redelivering webhooks.
These recipes connect Better IAM to the rest of your infrastructure: proving the audit log is intact, letting other
services trust who is calling, feeding your monitoring, and routing events to the systems that need them.
credential is the caller's credential ({ token } or { headers }).
Verify and archive the audit chain
The problem: an audit log only proves something if you can show that nobody edited it.
The solution: every tenant's audit records already form a hash chain, the
. Verify it with audit.verify, and copy it with audit.export to storage
your database administrators cannot rewrite. A later tampering attempt then shows up against your copy.
const status = await iam.api.audit.verify(credential, { tenantId }); // { valid, checked, head, failure? }
if (!status.valid) alert(`Audit chain broken at ${status.failure?.sequence}: ${status.failure?.reason}`);
let from = 1;
for (;;) {
const page = await iam.api.audit.export(credential, {
tenantId,
fromSequence: from,
limit: 5000,
});
await archive.append(page.body); // JSON Lines, in sequence order
if (!page.nextSequence) break;
from = page.nextSequence;
}better-iam audit-verify --config better-iam.config.mjs --tenant TENANT_ID
better-iam audit-export --config better-iam.config.mjs --tenant TENANT_ID --output audit.jsonl- The chain (
sequence,previousHash,hash) detects alteration, reordering, or removal by anyone who cannot rewrite both the events and the chain head. Treat your exported heads as the reference. - Archives verify anywhere with
verifyAuditChain(events, { previousHash })frombetter-iam, including in browsers and workers. - The CLI commands read storage directly: they need no credential and record no audit event.
audit-exportrefuses to overwrite an existing file. - For a scheduled, verified, write-once copy, configure
auditArchiveand runaudit-archiveinstead of a hand-written loop.
See Audit chain.
Call a downstream service with a stateless assertion
The problem: a reporting service or an internal API needs to know who is calling, and in which tenant. It should not call IAM on every request or hold the deployment secret.
The solution: issue an for that service with assertions.issue: a
short-lived signed token that describes the caller for one named audience. The service checks it with
verifyAssertion and a key derived from the secret.
// Issuer: the caller needs iam:assertions:create on iam/reports
const { token } = await iam.api.assertions.issue(credential, {
tenantId,
audience: 'reports',
ttlSeconds: 120,
});import { verifyAssertion } from 'better-iam';
// Holds only the derived key (iam.assertionKey()), never the secret.
// Throws INVALID_ASSERTION for a forged, expired, or wrong-audience token.
const claims = verifyAssertion(token, { key: process.env.IAM_ASSERTION_KEY!, audience: 'reports' });
// claims.sub, claims.tid, claims.roles, claims.groups, claims.mfaFrom a Next.js server component: await iamNext.assertion({ tenantId, audience: 'reports' }).
- Assertions are HS256 JSON Web Tokens describing the identity, tenant, session kind, MFA, sign-in method, role and group IDs, and optional public claims. Issuing one is authorized and audited, so administrators decide which roles may obtain tokens for which services.
- They grant nothing inside IAM, cannot be exchanged for sessions, and cannot be revoked before they expire.
ttlSecondsis 10 seconds to one hour (five minutes by default); keep lifetimes short. - The service holding
iam.assertionKey()cannot recover the secret, but treat the key as a shared secret. During a secret rotation, give servicesiam.assertionKeys();verifyAssertionaccepts a list. See Secrets and keys.
Observe latency and outcomes
The problem: you need IAM's latency, error rates, and denials in the same dashboards as the rest of your service, without adding a dependency.
The solution: set observability.onSpan. It hands you one span per unit of work, with its kind, name,
outcome, and duration, to feed any metrics library.
// In the betterIam() options:
observability: {
onSpan(span) {
histogram.observe({ kind: span.kind, name: span.name, outcome: span.outcome }, span.durationMs);
if (span.outcome === 'denied') counter.inc({ code: span.code ?? '' });
},
}- Spans cover operations, authorization checks, authentication calls, and HTTP requests.
outcomeisok,denied(401, 403, and 429 refusals and advisory denials), orerror, with the errorcode. - The handler must be synchronous and cheap; exceptions it throws are ignored.
- For a ready-made Prometheus endpoint, set
observability.metricswith a bearer token instead.
See Observability.
Webhooks: only denials, only some resources, and redelivery
The problem: a SIEM wants only denied actions, not every event. And after an outage at the receiving end, you need to send the missed events again.
The solution: filter the subscription with outcomes and resources, and
use its delivery history to redeliver what failed.
const { webhook, secret } = await iam.api.webhooks.create(credential, {
tenantId,
url: 'https://siem.example.com/iam',
events: ['*'],
outcomes: ['deny'],
resources: ['iam/*'],
});
// Store `secret` for the endpoint now: it is returned only once.
// After the SIEM was down: send every abandoned delivery again.
const history = await iam.api.webhooks.listDeliveries(credential, {
tenantId,
webhookId: webhook.id,
});
for (const delivery of history.filter((item) => item.status === 'failed'))
await iam.api.webhooks.redeliver(credential, {
tenantId,
webhookId: webhook.id,
deliveryId: delivery.id,
});outcomesandresources(glob patterns over the event'sresourceId) narrow a subscription beyond its event patterns.webhooks.updatechanges them, andnullclears them.- Endpoints must verify the HMAC signature and timestamp with
verifyWebhookSignaturebefore trusting a delivery. listDeliveriesreturns the newest deliveries (100 by default) with attempts, timestamps, the last error, and apending,delivered, orfailedstatus, never payloads.redeliverqueues the event again, rebuilt from the audit record and signed with the current secret. Endpoints must still deduplicate by eventid.- Deliveries leave through the outbox, so they need the
outboxjob.
See Webhooks.
Next steps
Better IAM is created by Sean Filimon
Last updated