The companion teardown on this site — “You are issuing identities at machine rate into a lifecycle built for people” — argues that the mismatch is structural rather than a matter of diligence: joiner-mover-leaver was built around an employment relationship, machine credentials have no employment relationship, and the gap widens every quarter because issuance is automated and deprovisioning is not. I am not going to re-argue that here. This piece assumes it and asks the question that follows, which is the one a platform engineer actually has to answer: what do you build, precisely, and what does it cost.

The scope is narrow on purpose. Not an enterprise governance programme, not a discovery tool that reconciles what already exists — a control plane inside a company that issues credentials to other companies' workloads. Cloud providers, model-serving platforms, agent runtimes, anything with a token endpoint that a customer's software calls. That position is unusual and it is the whole reason this design is possible: the platform is the issuer, and the issuer is the only party that can make a complete record a condition of getting a working credential. Everyone else is reduced to asking nicely and reconciling afterwards, which at current growth rates is a race the reconciler loses.

Three constraints rule out most of what you would build first. It has to work at platform scale, which kills any design that puts a synchronous call to the platform on every authorisation decision. It has to be multi-tenant safe, which kills any design where one tenant's revocation state is legible to another. And it cannot assume the platform can see inside the customer's policy — the platform does not know what a customer's agents are for, and should not — which kills any design where the platform decides what a credential is allowed to do. What survives all three is smaller than you would expect, and offline verification matters here more than it does anywhere else.

What has to be true before anything else

Start from the premise and follow it without shortcuts, because most of the design is forced and it is worth seeing which parts are choices.

The premise: an identity without a defined lifecycle owner and a tested revocation path is a liability, not an asset. Both qualifiers do work. Without an owner, there is no one to ask whether the thing is still needed, so it is never removed. Without a tested revocation path, the ability to remove it is a belief rather than a capability.

Tested is not a rhetorical flourish. RFC 7009, the OAuth token revocation specification, requires that “the authorization server responds with HTTP status code 200 if the token has been revoked successfully or if the client submitted an invalid token”. A 200 from a revocation endpoint is therefore compatible with the endpoint working perfectly, with the endpoint doing nothing at all, and with the token having been garbage in the first place. Response codes cannot distinguish these cases, which means the only evidence that a revocation path works is an observation, at a real resource server, that a credential which used to be accepted is now refused. Anything short of that is an assertion.

The specification is also candid that the property it describes is not instantaneous. It states that “the invalidation takes place immediately, and the token cannot be used again after the revocation”, and then immediately concedes: “In practice, there could be a propagation delay, for example, in which some servers know about the invalidation while others do not.” Every implementation lives in the second sentence. Its security considerations go further and note that access-token revocation support is itself optional — “if the authorization server does not support access token revocation, access tokens will not be immediately invalidated when the corresponding refresh token is revoked”, and that deployments must take this into account in their security risk analysis. Revoking a refresh token SHOULD invalidate the access tokens behind it; revoking an access token MAY invalidate the refresh token. The cascade is asymmetric and non-mandatory in the standard, which means an implementation can be fully conformant and still leave the grant alive behind a revoked credential.

From the premise, the first consequence: the record has to exist at the moment the credential does. Not soon after. A reconciliation process is a bet that discovery outruns creation, and machine-identity populations are not obliging on that point. Entro Labs, analysing over twenty-seven million non-human identities, publishes a forty-four percent year-over-year increase in sprawl, and reports that 7.5 percent of machine identities in cloud environments are between five and ten years old with over two percent of active secrets more than a decade old. Those are vendor figures drawn from Entro's own customer base — organisations the publisher describes as already prioritising machine-identity security — so they are a lower bound from a self-selected population rather than a census. A reconciler chasing that has to be right about a growing population and about a decade-long tail simultaneously. It is a losing shape.

Second consequence: if the record has to exist at issuance, something has to refuse to issue when it does not. That refusal has to be mechanical rather than procedural, because a procedural gate is a review meeting and review meetings get skipped under delivery pressure. Mechanically, there is exactly one chokepoint: the signing key. If the only code path to the key requires a committed registration, then an incomplete record cannot produce a working credential — not as a matter of policy, but as a matter of what the program is able to do.

Third: the record must be total. A partial record is worse than no record, because it produces a number that looks like coverage. The rule is that a record missing any required field is not partial, it is undefined, and undefined fails closed. This is the load-bearing move, and its value is not moral. It is that “undefined” is actionable in a way “too many” is not. An estate with 4,000 machine identities and no field set generates a meeting. An estate with 3,700 defined records and 300 undefined ones generates a work queue with 300 items in it, each of which has a specific missing field and therefore a specific person to ask.

Fourth, and this is the part specific to platforms: only the issuer can enforce any of this. A customer's security team can ask their engineers to complete a record, and their engineers can decline, and nothing happens. A platform can decline to sign. The asymmetry is total, and it is why this design belongs in a platform's control plane rather than in a governance tool bolted alongside it.

There is a regulatory reading of the same point, and it is shorter than people expect. In North America the revised interagency model risk guidance of 17 April 2026 — OCC Bulletin 2026-13, issued in parallel by the Federal Reserve as SR 26-2, superseding SR 11-7 and SR 21-8 — moved the ground. OCC Bulletin 2026-13 states in its own wording that generative and agentic AI models are novel and rapidly evolving and as such are not within the scope of that guidance, and that it does not set forth enforceable standards or prescriptive requirements. Read as a deferral rather than an exemption, the implication for a platform is direct: the separate guidance, when it lands, will be written against whatever the industry has already built. Platforms that have a defensible answer to “who authorised this credential and can you take it away” will find their answer described. Platforms that do not will find someone else's.

Most of the field set is already mandatory

The usual objection to a governance record is that it is metadata nobody will maintain. That objection is weaker than it looks, because most of the fields are already required by the specification the platform is probably already implementing.

RFC 9068, the JWT profile for OAuth 2.0 access tokens, specifies seven REQUIRED claims in section 2.2: iss, exp, aud, sub, client_id, iat and jti. Issuer, expiry, audience, subject, client, issued-at and a unique credential identifier. Those are not aspirational governance metadata invented by a risk function; they are mandatory in the profile. Which means a platform issuing conformant JWT access tokens already has issuer, principal, audience, credential identifier and expiry as a matter of compliance with the token format.

That leaves three fields that no token profile carries: the owner, the purpose, and the revocation binding. The whole governance argument reduces to three fields. When someone tells you the record is too heavy to maintain, the accurate response is that they are already maintaining five-eighths of it and calling it a JWT.

The unit that either exists or does not Five of the eight fields are already REQUIRED claims in the JWT access-token profile. Three are the governance fields no token profile carries. THE RECORD issuer which authority minted it RFC 9068 REQUIRED principal which workload holds it RFC 9068 REQUIRED audience which resource may accept it RFC 9068 REQUIRED material the key, and its unique id RFC 9068 REQUIRED expiry the moment it stops working RFC 9068 REQUIRED owner the human answerable for it GOVERNANCE FIELD purpose the class of action it exists for GOVERNANCE FIELD revocation the endpoint, and proof it works GOVERNANCE FIELD THE RULE A record missing any field is not partial. It is UNDEFINED. And undefined does not mint. Registration is a precondition of issuance, not a reconciliation afterwards. WHY ONLY THE PLATFORM A customer can be asked to complete a record, and can decline. The issuer can refuse. WHAT THE ABSENCE OF THE RULE LOOKS LIKE IN SHIPPED SYSTEMS Grants outlive principals Role assignments are not deleted when the managed identity is. They must be removed by hand, and afterwards read as Identity not found. Microsoft Entra documentation Deletion is not removal A deleted system-assigned identity still counts toward the limit until it is fully purged after thirty days. Your count depends on a flag nobody agreed. Microsoft Entra documentation A 200 is not a revocation The server responds 200 if the token was revoked successfully or if the client submitted an invalid token. The code cannot tell you which happened. RFC 7009, Section 2.2 Five fields are already mandatory. The governance record is three fields away from the profile most platforms already implement.

Two further things RFC 9068 already establishes matter for what comes later. First, it recommends asymmetric signing precisely so that resource servers can validate locally: “it is RECOMMENDED here that authorization servers sign JWT access tokens with an asymmetric algorithm”. The profile is already built on the assumption that verification happens without calling home. Second, when it enumerates validation, the only invalidation test it requires is time — “the current time MUST be before the time represented by the exp claim”. Expiry is the whole of it. A conformant verifier will accept a revoked credential until it expires, because the profile gives it nothing else to check.

That is the gap this design fills, stated exactly: the profile already assumes offline verification and already forces the identifying fields, and it has no revocation check. The design adds one — and adds it in a form that does not break the offline property, because breaking the offline property is how every previous attempt at this failed.

SPIFFE is worth reading here for a different reason. Its X.509-SVID standard enforces one identity per credential and makes ambiguity fail closed: “An X.509 SVID MUST contain exactly one URI SAN, and by extension, exactly one SPIFFE ID”, and “Validators encountering an SVID containing more than one URI SAN MUST reject the SVID.” It also requires that leaf certificate SPIFFE IDs have a non-root path component and that the key usage extension be set on all SVIDs and marked critical. That is a standards body reaching the same conclusion this design reaches from first principles: a credential whose subject is ambiguous is not a weaker credential, it is an invalid one. Its deployment posture is worth reading alongside that. SPIRE's documented server defaults are a default_x509_svid_ttl of one hour, a default_jwt_svid_ttl of five minutes, and a ca_ttl of twenty-four hours — short lifetimes carrying a great deal of the operational weight. I am not going to make a claim about what revocation machinery SPIFFE does or does not specify; I have read two of its standards and a negative asserted from two documents is not a fact about a project. What I will say is the thing that holds regardless: a design that leans on short lifetimes bounds exposure at the credential lifetime and says nothing about the grant behind it, which is the same gap RFC 7009's asymmetric cascade leaves open from the other direction.

The two fields that do the work

Owner and purpose are the fields that determine whether the record is governance or decoration, and both fail in a specific, predictable way if you specify them loosely.

Owner has to resolve, not merely exist. A free-text owner field decays into the name of somebody who left two years ago. The field must therefore be a directory principal that the registry re-resolves on a schedule, with a resolution expiry of its own: past it, the record returns to undefined until re-resolution succeeds. The failure this defends against is documented rather than hypothetical. Microsoft states plainly that “role assignments aren't automatically deleted when either system-assigned or user-assigned managed identities are deleted” and that they must be removed by hand, after which they appear in the portal as “Identity not found” with an ObjectType of Unknown. That is an entitlement whose principal no longer resolves — undefined by the system's own admission, sitting in a production access-control decision.

The same documentation supplies the reason a re-resolution schedule is not enough on its own: “While a deleted system-assigned identity is no longer accessible by any resource, it counts towards your limit until fully purged after 30 days.” Deletion is not removal. Whether a directory lookup returns “absent” depends on where in a soft-delete window you happen to ask, which means any population count you produce depends on a flag nobody agreed on. Publish which side of that line your census sits on, or your number is not comparable with anyone else's — including your own from last quarter.

Purpose has to be a reference, not a sentence. A free-text purpose is unfalsifiable, and an unfalsifiable field is decoration with a maintenance cost. But the platform cannot define the purposes either, because the platform does not know what its customers' agents are for and should not be in a position to find out. The resolution is that purpose is a reference into an action-class registry the tenant owns. The platform requires that the reference exists and is currently valid; it never interprets the semantics. That single decision is what keeps the design multi-tenant safe, because it means the platform can enforce a completeness rule over data it cannot read.

The revocation binding is the third new field and it is not a URL. It is an endpoint, a set of epoch keys ordered from narrowest to widest, and the timestamp of the last drill that observed a real refusal through it. If that timestamp is null, or older than the freshness policy, the binding cannot mint. The premise said “tested revocation path”; this is what makes the word “tested” into a compile-time and run-time fact rather than an adjective in a policy document.

The data model

What follows is the core of the reference package, written against a generic OAuth authorisation server and a generic directory, because that is the most common platform shape. Nothing in it depends on a particular cloud. The strict-mode typing is deliberate and load-bearing: every property is required, which means a caller cannot construct a partial record and a future contributor cannot add a field without every call site failing to compile. That is the completeness rule expressed in a place where it cannot be forgotten.

Configuration

@authority/nhi — record, mint, verify, drill

Four modules. The first defines what a credential record is; the second makes registration a precondition of signing; the third verifies without a network call; the fourth is the drill that produces the number everything else is measured against.

The unit of record as a total type. Note what is not representable: there is no unconstrained bearer credential, no optional owner, no empty audience, and no way to express a revocation binding that has never been proven except as an explicit null the mint path refuses.

packages/nhi/src/record.ts
// @authority/nhi — the unit of record.
//
// Every property is required. That is the design decision, not an accident of style:
// a total type means no caller can construct a partial record, and no future contributor
// can add a field without every call site failing to compile.

declare const brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [brand]: B };

export type TenantId = Brand<string, "TenantId">;
export type CredentialId = Brand<string, "CredentialId">;
export type EpochKey = Brand<string, "EpochKey">;
export type UnixSeconds = Brand<number, "UnixSeconds">;

/** RFC 9700 s2.2.1: authorization and resource servers SHOULD use mechanisms for
 *  sender-constraining access tokens, such as mutual TLS or DPoP. There is deliberately
 *  no "unconstrained" member here — a plain bearer credential is not representable. */
export type SenderConstraint =
  | { readonly kind: "mtls"; readonly certificateThumbprint: string }
  | { readonly kind: "dpop"; readonly jwkThumbprint: string };

export type CredentialMaterial =
  | {
      readonly kind: "jwt";
      /** RFC 9068 RECOMMENDS an asymmetric algorithm so resource servers validate locally. */
      readonly algorithm: "ES256" | "RS256" | "EdDSA";
      readonly senderConstraint: SenderConstraint;
    }
  | {
      readonly kind: "x509-svid";
      /** Exactly one SPIFFE ID. The X509-SVID standard requires exactly one URI SAN and
       *  requires validators to reject an SVID carrying more than one. */
      readonly spiffeId: string;
      readonly serialNumber: string;
    };

/** A directory principal that must still resolve — not a name typed into a form.
 *  resolutionExpiresAt is what stops this field decaying quietly: past it the record is
 *  undefined again until a re-resolution succeeds. */
export interface OwnerBinding {
  readonly kind: "person" | "group";
  readonly directoryId: string;
  readonly resolvedAt: UnixSeconds;
  readonly resolutionExpiresAt: UnixSeconds;
}

/** A reference into the tenant's own registry of action classes. The platform stores the
 *  reference and never interprets it. That is the multi-tenancy boundary: completeness is
 *  enforceable over data the platform cannot read. */
export interface PurposeBinding {
  readonly actionClassRef: string;
  /** Recorded for the audit trail. Never consulted in an authorisation decision. */
  readonly statement: string;
}

export interface RevocationBinding {
  /** The RFC 7009 revocation endpoint. */
  readonly endpoint: string;
  /** Ordered narrowest to widest: credential, principal, owner, tenant. Bumping a wider key
   *  revokes everything beneath it — which is how this design makes cascade mandatory
   *  rather than the SHOULD/MAY that RFC 7009 s2.1 leaves it as. */
  readonly epochKeys: readonly [EpochKey, ...EpochKey[]];
  /** Null until a drill has observed a real refusal through this endpoint. Null does not
   *  mint. */
  readonly lastProvenAt: UnixSeconds | null;
  readonly observedPropagationMillis: number | null;
}

export interface CredentialRecord {
  readonly credentialId: CredentialId;               // jti      — REQUIRED by RFC 9068
  readonly tenant: TenantId;
  readonly issuer: string;                           // iss      — REQUIRED
  readonly principal: string;                        // sub      — REQUIRED
  readonly audience: readonly [string, ...string[]]; // aud      — REQUIRED, never empty
  readonly material: CredentialMaterial;
  readonly owner: OwnerBinding;                      // governance field
  readonly purpose: PurposeBinding;                  // governance field
  readonly issuedAt: UnixSeconds;                    // iat      — REQUIRED
  readonly expiresAt: UnixSeconds;                   // exp      — REQUIRED
  readonly revocation: RevocationBinding;            // governance field
}

export type FieldName = keyof CredentialRecord;

const REQUIRED_FIELDS: readonly FieldName[] = [
  "credentialId",
  "tenant",
  "issuer",
  "principal",
  "audience",
  "material",
  "owner",
  "purpose",
  "issuedAt",
  "expiresAt",
  "revocation",
];

/** The only definition of "undefined" in the system. Every other module refers back here,
 *  so there is exactly one place to argue about what completeness means. */
export function missingFields(draft: Partial<CredentialRecord>): readonly FieldName[] {
  const missing = REQUIRED_FIELDS.filter((field) => draft[field] === undefined);
  if (draft.audience !== undefined && draft.audience.length === 0) {
    return [...missing, "audience"];
  }
  return missing;
}

Signature generation and validation are omitted deliberately: RFC 9068 already specifies that part and every platform of this shape already has it. What is shown is only the machinery the profile does not supply.

The control path

Nine steps, in order, with the properties that make each one necessary.

  1. A customer's control plane requests a credential for a workload. The request carries a draft record, not a scope string.
  2. The registry runs the completeness check. A missing field returns the field names, not a generic rejection — the error is the work queue.
  3. The owner is resolved against the customer's directory. Not active means not minted, regardless of how complete the rest of the record is.
  4. The revocation binding is checked for a fresh passing drill. This is the step that has no analogue in any issuance path I have read, and it is the one that makes the premise real.
  5. The requested lifetime is checked against the maximum the platform can defend, which is a function of the revocation window rather than a round number somebody liked.
  6. The record is committed durably. This write is the moment the credential conceptually exists, and it precedes the signature so that a crash produces a record without a credential rather than the reverse.
  7. The signer is called with the receipt. It has no other entry point.
  8. The credential is returned carrying its epoch keys and their current values, which is what makes offline revocation checking possible later.
  9. The distribution plane begins publishing a signed bundle containing every epoch key that has ever been bumped, on a cadence whose staleness bound is the platform's published revocation figure.
Five planes, and what each may not do The constraints on the right are the design. The boxes on the left are only where the code lives. PLANE THE CONSTRAINT Tenant policy plane CUSTOMER-OWNED Action classes. Approvals. The authorisation rules that decide whether a given call is allowed at all. The platform must not see inside this. Registry plane PLATFORM The record. Owner resolved against the directory. Purpose stored as an opaque tenant reference. Revocation bindings, each with its last proven drill. Incomplete here means no credential. Issuance plane PLATFORM The signer. It takes a registration receipt, not a record. Asymmetric keys, so verifiers work locally. No path to the key without a record. Distribution plane PLATFORM A signed epoch bundle. Deltas only: the keys that have ever been revoked, and nothing else. Its staleness bound is the published revocation figure. One way. Push, never query. Verification plane AT THE RESOURCE SERVER Three local checks: signature, expiry, epoch. No network call. Fail closed once the bundle is stale beyond the tolerance you chose and published. Offline, or it does not survive scale. THE DRILL, running across all five: a canary credential per binding, revoked on a schedule, polled at a real resource server until refused. A binding whose drill has never passed is not permitted to mint.

The ordering in steps six and seven is the one architectural decision in this design that people argue about, and the argument is worth having explicitly. Committing before signing means a registry outage becomes an issuance outage: no credentials are minted while the registry is down. That is a real availability cost and it should be priced. The alternative — sign, then record asynchronously — buys availability by admitting exactly the failure mode this design exists to eliminate, because the queue will drop messages and the dropped ones become credentials nobody has a record of. Every estate that has this problem got it from a queue somebody thought was reliable. Take the availability cost, and make the registry a tier-zero service with the operational treatment that implies.

Revocation you can prove

The revocation design has to satisfy three things at once: it must work without a per-request call to the platform, it must make cascade mandatory rather than optional, and it must produce a number a customer can be told.

The mechanism is a monotonic counter per epoch key. Every credential carries the current value of each of its keys at the moment it was minted. Revocation is a bump of the counter, and verification is a comparison: if the bundle's value for a key exceeds the value in the credential, the credential is dead. Keys are hierarchical, ordered narrowest to widest — this credential, this principal, this owner, this tenant — so revoking a principal kills everything issued under it in one write, and revoking a tenant kills everything in one write.

That hierarchy is the answer to the asymmetry in RFC 7009. The specification leaves cascade as a SHOULD in one direction and a MAY in the other, which means a conformant implementation can revoke a credential and leave the grant behind it fully alive. With epoch keys, the grant is a key. You cannot revoke the credential without deciding, explicitly, which level you are revoking at — and both the decision and the level end up in the record.

The alternative most platforms reach for first is token introspection, and RFC 7662 is unusually honest about what it costs. It states that the response “MAY be cached by the protected resource to improve performance and reduce load on the introspection endpoint, but at the cost of liveness of the information used by the protected resource to make authorization decisions”, and requires that a response carrying exp “MUST NOT be cached beyond the time indicated therein”. Its security considerations then name the failure directly: “the token may be revoked while the protected resource is relying on the value of the cached response to make authorization decisions. This creates a window during which a revoked token could be used at the protected resource.”

That is a precise statement of an unavoidable trade, and it is why introspection does not survive the platform-scale constraint. Do not cache, and every authorisation decision anywhere in your customers' estates takes a synchronous dependency on your availability — you have made yourself a single point of failure for other companies' request paths, at their traffic volumes rather than yours. Cache, and you have a revocation window exactly as long as your cache TTL, which is the thing you were trying to avoid. The epoch bundle does not escape the trade; nothing escapes the trade. What it does is move the window to a single artefact with a signed expiry, so the window is one number you can publish, measure and defend rather than an emergent property of thousands of independently configured caches.

What actually happens when you call revoke Everything before the last stage is intent. Only the last stage is evidence. The interval worth publishing runs from stage 1 to stage 6 — not from stage 1 to the 200. 1 revoke called HTTP 200 returned. Also returned for an invalid token, so this tells you nothing. 2 epoch bumped Durable, linearizable write in the registry. First moment at which anything is true. 3 bundle re-signed The delta list, plus the new staleness bound. 4 distributed Pushed outward. The long leg — and the one you own. 5 verifier refreshes Local copy replaced. 6 refused in fact Observed at a real resource server. The only evidence there is. THREE SHIPPED SYSTEMS, PRICING THE SAME DELAY RFC 7009 The invalidation takes place immediately, and the token cannot be used again after the revocation. In practice, there could be a propagation delay, in which some servers know about the invalidation while others do not. Section 2.1 AWS role sessions Revocation attaches a deny policy covering sessions taken in the past and approximately 30 seconds into the future — to absorb the policy's own propagation delay. The CLI caches credentials until they expire. AWS IAM User Guide Managed identities The back-end services maintain a cache per resource URI for around 24 hours — so role-membership changes can take several hours, and it is not possible to force a token to refresh before its expiry. Microsoft Entra documentation Every one of those numbers is a property of somebody's cache. If you do not publish yours, you do not have an objective. You have a hope.

Two shipped systems show the same arithmetic being done in public. AWS revokes role sessions not by killing credentials but by attaching an inline deny policy named AWSRevokeOlderSessions, and its documentation explains that “the policy denies all access to users who assumed the role in the past as well as approximately 30 seconds into the future. This future time choice takes into account the propagation delay of the policy in order to deal with a new session that was acquired or renewed before the updated policy is in effect in a given region.” The mechanism is a time-conditioned deny using DateLessThan against aws:TokenIssueTime, and the same page notes that “the AWS CLI caches credentials until they expire”, so the local cache has to be cleared separately. A thirty-second forward window is a published, defended estimate of a propagation delay. That is precisely the artefact this design asks a platform to produce.

Microsoft's managed identity documentation makes the opposite choice visible: “the back-end services for managed identities maintain a cache per resource URI for around 24 hours”, therefore “it can take several hours for changes to a managed identity's group or role membership to take effect. Today, it isn't possible to force a managed identity's token to be refreshed before its expiry.” That is also a published number, and it is a very large one. Neither vendor is doing anything wrong; both are pricing the same physics. The difference between them and most platforms is only that they wrote the number down.

The drill is what produces your number. A canary credential per binding, minted through the production path, presented at a real resource server, revoked, then polled until refusal. What it returns is not the time to a 200; it is the interval from the revocation call to the observed refusal, which is the only interval a customer cares about. Run it continuously, alert on regression, and make a fresh pass a precondition of minting. A revocation path that has never been exercised is indistinguishable from one that does not work, and the drill is what removes the word “indistinguishable”.

What the platform must not know

Multi-tenancy imposes constraints that are easy to violate accidentally, and two of them fall out of the design above.

The first is that purpose is a reference the platform stores and does not interpret. The consequence is that the platform can never answer the question “is this agent doing something it should not”, and it should stop trying. It can answer “does this credential have a purpose reference the customer registered, an owner who still exists, and a revocation path that was proven to work this morning”. That is a smaller claim and it is one the platform can actually stand behind, which makes it worth more than a larger claim it cannot.

The second is that the epoch bundle must not leak tenant topology. A shared global bundle would let any verifier infer how many credentials another customer revoked and when — a mass revocation is an incident signal, and incident signals about your customers are not yours to broadcast. The mitigations are per-tenant bundles where the verifier population is tenant-scoped, and opaque, salted epoch keys where it is not, so that a key reveals nothing about the principal it names. Bundle sizes leak a little regardless, and padding to a bucket boundary is the cheap answer. I would not claim this closes the channel completely; it narrows it to a coarse count, which is a defensible place to stop.

A third constraint is easier to state than to hold: the customer's authorisation policy stays on the customer's side of the line. This design gates issuance and revocation, and nothing else. Whether a given call is permitted is a decision made by the tenant's own policy plane against the tenant's own rules. A platform that starts making those decisions has taken on a liability it cannot scope and an obligation it cannot discharge across thousands of customers with incompatible risk appetites.

Where this design breaks

A design piece without a failure analysis is marketing with type annotations. These are the failure modes I can find in my own design, ordered roughly by how much they would worry me in a production review.

The window does not close, it only becomes legible. Between the epoch bump and the last verifier's refresh, a revoked credential still works. The bundle does not eliminate that interval; it collapses it into one measurable, signed number instead of thousands of unmeasured ones. Anyone who tells you their revocation is instant is claiming something RFC 7009 itself explicitly declines to claim. The honest framing is that you have chosen a window and can prove what it is.

Failing closed on a stale bundle is an outage mode wearing a security costume. If distribution breaks and verifiers refuse everything, you have converted a security window into a customer-visible availability incident, possibly across every tenant simultaneously. Two thresholds help — a soft one that alarms while continuing to serve, a hard one that refuses — but they do not dissolve the trade, they only let you pick a point on it. What I will defend is that the current default in most estates is a bundle that never expires and a verifier that never refuses, which is the same thing as having no revocation at all while feeling as though you do.

Bundle growth has a long tail. Deltas keep the structure small: only keys that have ever been revoked appear. But a key cannot be dropped until every credential that could carry it has expired, which means a single long-lived credential pins its epoch entries for its whole life. Short maximum lifetimes keep the bundle small and long ones inflate it, so the mint policy's lifetime cap and the bundle's size are the same dial viewed from two ends. I have not measured this on a real population, and the honest statement of the arithmetic is that bundle size is the count of revoked-and-not-yet-drained keys times a small constant. Whether that count is thousands or millions on a large platform is exactly the thing to measure before committing to the design.

It cannot reach caches held by other people. This design revokes at the resource server, because that is the only place it can act. If a customer's own service accepts a platform-issued credential without verifying it — or verifies it once and caches the result for an hour — nothing here reaches them. AWS's own guidance is the honest precedent: after revoking sessions, operators are told to clear the CLI's local credential cache by hand. A control plane that claims to reach every cache in every customer's estate is claiming something no vendor with published documentation claims.

It covers only what the platform mints. A gate at the signing key is powerless over a static secret a customer pasted into a repository. GitGuardian reports that 28.65 million new hardcoded secrets were added to public GitHub commits in 2025, a 34 percent year-over-year increase, and that the internal repositories they see are roughly six times more likely than public ones to contain hardcoded secrets — which is a statement about prevalence rather than about volume. Their retest figures are worse: nearly 70 percent of credentials confirmed valid in 2022 were still valid in January 2025, and 64 percent remained valid when retested in January 2026 — meaning they had not been remediated. That is a population this design never touches, and it is not a small one. Two caveats on those numbers, since they get misused: they count secret occurrences added to commits rather than distinct credentials, and they are a measure of secrets sprawl rather than an identity count. Do not put them next to a machine-to-human identity ratio.

Owner records decay, and the decay is bounded rather than solved. People leave. A directory keeps resolving them for a while — Azure's thirty-day purge window is the concrete case, and “deleted but still counted” is a state that will produce a resolvable lookup and a meaningless owner simultaneously. Re-resolution on a schedule bounds the staleness to the schedule interval and no further. Group owners survive departures but weaken accountability, because “the platform team” cannot be asked why a credential exists in the way a person can. I do not have a clean answer to this. The design makes the decay visible and puts a clock on it; it does not stop it.

Purpose can be made meaningless by a determined tenant. Nothing stops a customer defining one action class called “operations” and pointing ninety percent of their credentials at it. The design cannot prevent that, and claiming otherwise would be dishonest. What it can do is make the concentration legible: a tenant whose purpose distribution is one class covering nearly everything is a finding that shows up in a query. Legibility is a weaker property than enforcement, and it is the strongest property available to a party that has correctly refused to read the policy.

Clock skew turns two checks into an outage vector. Both the expiry test and the bundle staleness test are time-relative. Skew larger than the configured tolerance produces spurious refusals; tolerance larger than the skew extends every window by the tolerance. This is a well-understood problem with well-understood mitigations, and it is worth naming because a design that adds a second time-dependent check has doubled its exposure to the estate's worst clock.

It does nothing about composition. Three individually reasonable grants can combine into an authority nobody granted. That risk is real, it is the most interesting problem in this area, and it lives entirely in the tenant's policy plane. A registry that knows who owns a credential and can take it away has not made any statement about what the credential's holder can compose. Anyone selling this design as an answer to that question is selling something else.

What it costs

Some of these I can state as engineering facts about the design. Others are measurements I have not made, and I am going to be explicit about which is which, because a design piece that invents numbers is worse than one that admits to gaps.

Mint-path latency. The gate adds a directory resolution, a drill-freshness lookup and a durable registry write to the issuance path. The first two should be cached or precomputed — resolve the owner when the binding is created and on a schedule thereafter, not on every mint — which leaves the durable write as the only unavoidable addition. That write's cost is a replication latency, and replication latency is a property of the datastore, not of this design. The prediction under test is that a mint-time gate adds single-digit milliseconds at the median and is dominated at the tail by the registry's own commit behaviour under a thundering-herd restart, when every workload in a large customer re-mints at once. I have not measured this on a production platform and will not pretend a number.

Verification cost. Three local checks and a handful of map lookups against an in-memory structure. There is no network call, so the cost is dominated by the signature verification the JWT profile already requires, which the resource server was already paying. The prediction under test is that adding the epoch check is not measurable against signature verification in a request path. That is a claim about arithmetic and it is cheap to falsify — which is the point of stating it as a prediction.

Operational burden, which is where the real cost is. You acquire a tier-zero service in the issuance path, a signing key for the bundle with its own rotation and its own compromise story, a distribution path with its own monitoring, and a drill running continuously against production resource servers. Your on-call surface grows by at least: registry unavailable, bundle stale, bundle distribution lagging, drill failing, drill regressing. Realistically this is a platform team's quarter to build and a permanent addition to a rota, not a sprint. Anyone scoping it as a sprint has not counted the drill.

Migration, which is harder than the build. You cannot retro-register an existing population honestly, because the fields you would be filling in are exactly the ones nobody recorded — who owns it and what it is for are not recoverable from a token. The workable sequence is to gate new issuance first, classify everything already minted as undefined, publish the two counts side by side, and set a date after which undefined credentials stop being renewed rather than being revoked. Renewal is where the leverage is: most machine credentials are re-minted on a cycle, so a renewal gate converts the whole population within one maximum lifetime without a single forced revocation. The credentials that never renew are the ones you most needed to find, and they will surface as the residue.

One cost that is not obvious until you are in it: the gate will block a customer's deployment on day one, and the blocked field will usually be the owner. That is the design working as intended and it will not feel like it at the time. Decide in advance who is allowed to grant a temporary exemption, how long it lasts, and where it is recorded — because if you do not, the exemption will be an environment variable that somebody sets permanently in week three.

If you only had a week

Not the whole design. The smallest thing that produces a fact your organisation does not currently have.

  1. Write the record type and make it total. No optional fields. This is an afternoon and it forces the argument about what completeness means to happen once, in a pull request, rather than repeatedly in meetings.
  2. Instrument the existing mint path to emit what it would have recorded — and do not gate anything yet. Within a day you have a census: how many credentials your platform issued this week that could not have produced a complete record, and which field was missing in each case.
  3. Build the drill against exactly one revocation binding. Mint a canary, present it, revoke it, poll until refusal. The output is a number, and it will be the first time anyone in the building can say what your revocation interval actually is.
  4. Turn the gate on for one new credential type — ideally one used by an internal team, so the first blocked deployment is yours.
  5. Publish the drill's number as an objective, internally at minimum. A number nobody has committed to is a measurement; a number somebody has committed to is a control.

What you have at the end of that week is not a control plane. It is two numbers — how much of this week's issuance was undefined, and how long revocation actually takes — and a mint path that cannot produce an undefined record for one credential class. Both numbers are things most platforms cannot currently state, and both are the kind of fact that changes what the next quarter's roadmap looks like. Everything else in this piece is an elaboration of those two measurements.

The reason to start at the issuer rather than anywhere else is worth repeating in closing, because it is the only genuinely structural advantage in the whole problem. Every other party in this system can be asked to keep good records and can decline. The platform holding the signing key does not have to ask. It can simply decline to sign, and an incomplete record stops being a documentation gap and starts being a compile error in someone's deployment pipeline. That is a rare position to be in, and at the moment almost nobody who holds it is using it.