An agent runtime provisions a workload identity per agent instance. This is the correct design. You want per-instance identity because you want the audit trail to distinguish one agent run from another, because you want blast radius bounded to a single instance when something goes wrong, and because the alternative — a shared credential across a fleet — is the thing every security review has been telling you to stop doing for two decades. Nobody sensible builds it the other way.

The instances are ephemeral. They come up to serve a task, they hold context for the length of that task, and they exit. Sometimes they exit cleanly. More often, at any real rate, they are killed: the node is drained, the pod is evicted, the container hits its memory ceiling, the scheduler rebalances, the customer cancels the request midway through a tool call and the supervisor tears the whole tree down. In none of those cases does an orderly shutdown handler run to completion, and if you have ever instrumented a fleet you already know roughly what fraction of exits are of this kind. It is not small.

The identities are not ephemeral, because deprovisioning is a different kind of operation from issuance. Issuance sits on the request path: the workload cannot serve its first request until it has an identity, so a failed issuance blocks the workload, breaks a latency objective, shows up on a dashboard somebody watches, and gets retried until it succeeds. The retry loop for issuance exists, is well tested, and has an owner. Deprovisioning sits off the request path: it happens after the workload is gone, when nothing is waiting on it, when no customer is blocked by it, and when the failure has no consequence anybody has attached an alarm to. So it is best-effort. Its failures are logged at debug level, which is to say they are logged into a stream that is sampled in production and dropped at the collector.

What follows is a constructed illustration built from documented platform defaults, not an account of any incident at any company. A year into running that arrangement, the identity store holds a large population of credentials whose owning workload no longer exists. Each one is individually harmless — it was scoped narrowly, it authenticates to a service that has its own authorisation checks, and nothing in the estate is currently doing anything wrong with it. Collectively, it is an inventory nobody can attest to. And the word in that sentence carrying all the weight is not large, and it is not credentials. It is attest.

The objection that deserves to be answered first

The strongest response to everything above is that it is a solved problem dressed up as a new one, and that the person raising it has not looked at what a modern platform observability estate actually contains. That response deserves stating at its full strength rather than in a weakened form I can knock down.

Observability at platform scale is genuinely excellent and genuinely expensive. A large cloud or SaaS platform emits structured events for every control-plane mutation, including every credential issuance and every credential deletion, with actor, timestamp, request identifier, source address and result. Those events land in an append-only audit store with retention measured in years, are queryable in seconds across trillions of rows, are replicated across regions, and are themselves subject to integrity controls because the audit log is a compliance artefact in its own right. Distributed tracing links a credential issuance to the request that caused it and the deployment that requested it. Metrics on issuance and deletion rates are on somebody's wall. There are teams whose entire remit is that the audit stream is complete and tamper-evident, and they are good at it. Telling those teams their logging is inadequate is both insulting and, as stated, usually wrong.

It is also true that the platforms have not been idle on the lifecycle itself. Kubernetes moved its default away from the non-expiring Secret-based service-account token to a projected token that expires automatically and is rotated by the kubelet, and the documented reason is the one that matters here: the legacy Secret-based token does not expire, and a credential that never expires is a credential nobody re-examines. SPIFFE and SPIRE reduced the problem by making credentials so short-lived that the question of revoking them mostly stops arising — the SPIRE server ships with a one-hour default X.509 SVID lifetime, a five-minute default JWT SVID lifetime, and a twenty-four-hour signing-key lifetime. The OAuth security best current practice tells you to sender-constrain access tokens with mutual TLS or proof of possession so that a stolen token is not a usable token. None of this is a field standing still.

So the objection is not that logging is fine. The objection is sharper than that: the events are all there, the credential hygiene has genuinely improved, and if a customer wants to know what happened, the platform can tell them with precision and speed that would have been science fiction fifteen years ago.

Here is exactly what that does not answer. An audit log is a record of events. The question that fails is a question about state. “How many live machine identities exist right now that can reach this system, and who owns each one” is not a question about anything that happened; it is a question about what currently is. You can in principle reconstruct a state from a complete event stream — that is what an event-sourced system does — but only if three conditions hold: the stream is complete, every event is typed well enough to fold, and there is a stable key to fold on. In practice the third condition is the one that fails, and it fails for a reason that has nothing to do with logging quality.

There is no stable key joining a credential to the workload that held it to the human or team accountable for that workload. The issuance event knows which service principal called it. The deletion event, when it occurs, knows the credential identifier. Neither knows the agent instance, the tenant workflow it was serving, the deployment that spawned it, or the person who could be asked why it exists. That linkage was never a field anybody was required to populate, because at human rate you did not need it: a joiner-mover-leaver process has a human resources system as its authoritative key, and that key is durable, singular and externally maintained. There is no equivalent for a workload that lived for ninety seconds.

Second, and more decisively: absence produces no event. A deprovisioning that never ran writes nothing. There is no log line for a thing that failed to happen off the request path, so the residue is precisely the part of the population that is invisible to an event stream, by construction. The observability estate is complete with respect to actions taken and silent with respect to actions not taken — and the entire finding lives in the second category.

Third, the reconstruction is being asked of the wrong party. Every customer answering an authority question about agents running on your platform is folding your event stream into a state you did not emit, using join keys you did not provide, against a definition of the countable object that you have never published. Thousands of customers are independently building the same fragile pipeline, each getting a different answer, and every one of those answers is a claim about your platform's behaviour that you have no way to confirm or correct.

The forward path is loud. The reverse path is not. The residue is not sloppiness. It is the arithmetic of two paths with different failure semantics. ISSUANCE — ON THE REQUEST PATH Workload starts The runtime asks for an identity before the first request can be served. Issuance Synchronous. Sits on the critical path of the thing the customer asked for. Failure is loud A failed issuance blocks the workload, breaks a SLO and pages someone. Retried to success Nothing ships without it, so the retry loop exists and is well tested. DEPROVISIONING — OFF IT Instance exits Frequently by being killed, so no orderly exit handler ever runs. Deprovision tried Asynchronous, after the fact, off the path anyone measures. Failure is silent Logged at debug. Blocks nothing, breaks no SLO, pages nobody. Not retried The credential outlives the workload that held it, indefinitely. WHAT THE TWO PATHS COMPUTE A loud forward path and a quiet reverse path leave exactly one state variable in the system, and it only grows. Individually every stranded credential is harmless. Collectively they are an inventory nobody can attest to. Constructed illustration of a documented default pattern, not a report of any incident.

There is no agreed definition of the thing being counted

Before the mechanism, the measurement — because the measurement is in worse shape than the mechanism, and the state of the measurement is itself the strongest evidence that this is structural.

The machine-to-human identity ratio circulates as though it were a physical constant being refined. It is not. The published figures span an enormous range, and the range is not error bars around a single quantity. Each publisher counts a different object. Some count service accounts in a directory. Some count workload identities in a cloud tenancy. Some count secrets in a vault. Some count API keys observed in use. Some count credentials ever issued, whether or not anything still holds them. These are measurements of different quantities sharing a label, and averaging them, bracketing them, or citing the highest one is not analysis.

I will be concrete about my own evidence rather than assembling a range I cannot stand behind. Of the widely circulated ratios, the one I read at the publisher's own source this pass is Entro Labs' figure of 144 non-human identities per human user for the first half of 2025, published alongside a stated 44 per cent year-over-year increase in sprawl and a 56 per cent year-over-year rise in the ratio itself, drawn from analysis of over 27 million non-human identities and hundreds of thousands of secret-exposure incidents across enterprise environments. That is vendor telemetry from customers who bought a machine-identity product — a population, not a census — and the publisher is reasonably clear about the provenance.

What the publisher's own public summary of that figure does not do, anywhere, is enumerate what counts as a non-human identity. No list of object types appears. No statement of whether a service account and the four API keys attached to it are one identity or five. No statement of whether a credential issued and then deleted last week is in the denominator. The single most-quoted ratio in this conversation is published without a stated counting unit. Other ratios circulate in the same discussions, published by other vendors from other telemetry; I have not read those publications at source in preparing this piece, so I am not going to repeat their numbers. That is not fastidiousness for its own sake — the way these figures travel, detached from their definitions, is precisely the phenomenon under examination.

The definitional problem is not a vendor problem, and this is where the argument gets interesting for anyone who builds a platform. Take the ambiguity to the primary documentation and it does not resolve. It gets worse, because the platforms themselves define incompatible countable objects, each with a straight face and each entirely reasonable in its own context.

  • A directory principal. Microsoft's own guidance states that a deleted system-assigned managed identity “counts towards your limit until fully purged after 30 days”. So a count of managed identities in a tenant includes objects that have already been deleted, for a month. Whether your census includes soft-deleted objects changes the number, and both answers are defensible.
  • A role session. An AWS assumed-role session is a set of temporary credentials with real authority and no corresponding stored identity object to enumerate. Revoking those sessions does not delete anything: it attaches an inline policy to the role that denies access to anyone who assumed it in the past, and roughly thirty seconds into the future, because the policy itself takes time to propagate. There is nothing durable in that mechanism to put in a population count.
  • A service-account token. Kubernetes carries two incompatible species under one object. Legacy Secret-based tokens, in the project's own words, “don't expire and don't rotate”; the modern default is a projected token that expires automatically and is rotated by the kubelet before it does. One ServiceAccount can be associated with either. A count of service accounts tells you almost nothing about how many usable credentials exist.
  • An SVID. Under SPIRE's documented defaults — one hour for an X.509 SVID, five minutes for a JWT SVID — a single long-running workload emits a continuous stream of distinct credentials. Counting issued credentials in a SPIFFE estate measures a flow rate. Counting distinct SPIFFE IDs measures something entirely different, and the ratio between the two is a configuration parameter.
  • A secret occurrence. GitGuardian reports that 28.65 million new hardcoded secrets were added to public GitHub commits in 2025, a 34 per cent year-over-year increase, and notes that internal repositories are roughly six times more likely than public ones to contain hardcoded secrets — a statement about prevalence, not about volume. That is a count of secrets appearing in source-control commits within a stated population of public repositories. It is a real, carefully scoped measurement of something that is not an identity count at all, and it should never be placed beside a ratio as though it were.

Five objects. Five defensible readings of one phrase. They differ by orders of magnitude, and a platform engineer asked “how many machine identities does your product create” cannot answer wrongly, because there is no definition against which the answer could be wrong.

One label, five countable objects Every row below is defined by a platform's own documentation. Every row is a defensible reading of the phrase “machine identity”. Directory principal Microsoft Entra managed identity A deleted system-assigned identity “counts towards your limit until fully purged after 30 days”. Whether it is in your count depends on whether soft-deleted objects are included. Counts objects that have already been deleted. Role session AWS STS assumed-role session A session is a set of temporary credentials, not a stored identity object in a directory. Revocation is a deny policy on the role, effective about 30 seconds into the future. Nothing durable exists to enumerate. Service-account token Kubernetes ServiceAccount Legacy Secret-based tokens “don't expire and don't rotate”. Projected tokens expire automatically and are rotated by the kubelet before they do. Two different species under one object. SVID SPIFFE / SPIRE server defaults default_x509_svid_ttl is 1h; default_jwt_svid_ttl is 5m; ca_ttl is 24h. One long-running workload therefore emits a continuous stream of distinct credentials. The unit is a flow rate, not a population. Secret in a commit GitGuardian, public GitHub only 28.65 million new hardcoded secrets were added to public GitHub commits in 2025. The publisher's population is public commits; internal repositories are excluded. Counts occurrences in source control. These are not five estimates of one quantity with different error bars. They are measurements of five different quantities sharing a label, and they differ by orders of magnitude. A published ratio that does not name its row is not a measurement. It is a number with a label attached. Sources: Microsoft Entra managed-identity best practices · AWS IAM User Guide · Kubernetes Service Accounts · SPIRE Server Configuration Reference · GitGuardian, State of Secrets Sprawl 2026.

Why the residue is structural

It is tempting to file all this as a maturity problem — the kind of thing a determined platform team closes in two quarters with a reconciliation job and a dashboard. I think that reading is wrong, and I want to give the reason precisely, because “structural” is a word that gets used to mean “hard” when it should mean something stronger: that the outcome is what the design computes, so that fixing it requires changing what is emitted rather than working harder on what already is.

First, the two paths have asymmetric failure semantics, and asymmetry integrates. Issuance is synchronous, blocking, alarmed and retried. Deprovisioning is asynchronous, non-blocking, unalarmed and unretried. This is not an accident of anybody's implementation; it follows from where the two operations sit relative to the customer request. Any system with a reliable forward path and a best-effort reverse path has a monotonically increasing state variable, and the rate of increase is the product of the issuance rate and the deprovisioning failure rate. Both terms are moving in the wrong direction: agent runtimes raised the issuance rate by orders of magnitude, and higher instance churn raises the fraction of exits that are kills rather than clean shutdowns. You cannot fix a monotone accumulator by improving the constant. You fix it by closing the loop, which means making the reverse path observable in the same way the forward path already is.

Second, the protocol layer offers expiry where the lifecycle needs revocation. This is the deeper point, and it is not a criticism of the specifications, which are careful and honest about what they do. It is an observation about what the specifications were built to carry.

RFC 7009, the OAuth token revocation specification, makes cascading revocation both asymmetric and non-mandatory. If the token presented is a refresh token and the server supports access-token revocation, the server SHOULD also invalidate the access tokens issued under the same grant; if the token presented is an access token, the server MAY revoke the corresponding refresh token. Read that in the other direction, which is the direction an incident actually runs: revoking an access token does not, by the specification, kill the grant behind it. It also does not necessarily do anything at all, because the same document's security considerations note that 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 account for this in their risk analysis — support for revoking access tokens is optional in the specification, not assumed.

The specification is also candid that the operation is not atomic across a distributed system.

The invalidation takes place immediately, and the token cannot be used again after the revocation. In practice, there could be a propagation delay, for example, in which some servers know about the invalidation while others do not.
RFC 7009, OAuth 2.0 Token Revocation, Section 2.1

No bound is given on that delay, and none could reasonably be given by a specification that does not know your topology. Then there is the response code, which is the detail I would put in front of any team that believes their revocation path works: the authorization server responds with HTTP 200 if the token has been revoked successfully or if the client submitted an invalid token. A 200 is therefore compatible with the token having been revoked, with the token never having existed, and with a revocation service that quietly stopped writing to its store three deploys ago. An untested revocation path cannot be distinguished from a working one by response code alone. If your integration test asserts on the status code, it asserts on nothing.

Introspection does not rescue this, and RFC 7662 is unusually explicit about the trade it is asking you to make. The response may be cached by the protected resource to improve performance and reduce load on the introspection endpoint — but, in the specification's own words, at the cost of liveness of the information used to make authorization decisions, and it must not be cached beyond any expiry the response indicates. The security considerations then name the failure mode outright: the token may be revoked while the protected resource is still relying on the cached response, creating a window during which a revoked token could be used. That window is a configuration choice made independently by every resource server in the path, and the party performing the revocation does not control any of them.

The self-contained token profile takes the last step. RFC 9068 recommends that authorization servers sign JWT access tokens with an asymmetric algorithm precisely so that resource servers can validate them locally, without a network call — which is exactly what you want for latency and availability, and which necessarily means the resource server is not asking anybody whether the token is still good. Its required validation is that the current time is before the time in the expiry claim. Expiry is the only invalidation check the profile requires. A locally validated JWT is, by design, a bearer assertion that nothing can withdraw before it expires.

Put those together and a coherent design philosophy comes into focus, and it is a good one: prefer short lifetimes to revocation machinery, because revocation across a distributed system is slow, partial and hard to verify, whereas expiry is local, cheap and unfailing. SPIRE's defaults are that philosophy expressed as configuration. The OAuth security best current practice's own documented mitigation runs the same way — sender-constrain the token with mutual TLS or proof of possession so that theft stops paying — rather than promising that revocation will save you.

Third — and this is the crux — the same substitution that fixes credential hygiene destroys identity accounting. If your answer to lifecycle risk is that credentials live for an hour, then the durable thing in your system is no longer the credential. A one-hour credential is an event; it is not an object anyone can inventory, and by the time a question is asked about it, it has expired thousands of times over. Meanwhile the thing that persists — the identity, the trust-domain path, the role, the service account, the entitlement attached to it — has no lifecycle of its own at all, because everybody's attention went to the credential.

This is the conflation at the centre of the whole problem. A human lifecycle assumes one identity, one durable record, and a small number of credentials attached to it, with joins and leaves arriving at human rate through an authoritative external system. An agent runtime inverts every one of those terms: many identities per workload, no durable record, an unbounded stream of credentials, and joins and leaves arriving at machine rate with no authoritative external system anywhere in the picture. We took a lifecycle whose invariants were about people and pointed it at a population that violates every one of them.

What “revoke” actually does, step by step

Abstractions about lifecycle are easy to nod along to, so it is worth walking the mechanism at the level a responder actually experiences it. Take the moment when someone concludes that a particular agent identity must stop working — a prompt-injection finding, a tenant offboarding, a customer demanding an immediate cut-off. Trace what happens on each of the major substrates, using only what each vendor documents.

  1. You call the revocation endpoint. You receive a 200. Per RFC 7009 §2.2, that 200 is also the response to an invalid token, so it establishes only that the endpoint was reachable and parsed your request. You have learned nothing about the credential.
  2. You assume the grant behind the credential is now dead. Per RFC 7009 §2.1 the cascade runs the other way: a refresh token SHOULD take its access tokens with it, an access token MAY take its refresh token. If you revoked the access token, the grant may well still be able to mint another.
  3. Resource servers that introspect are consulting a cached answer. Per RFC 7662 §2.2 that cache is a deliberate performance trade against liveness, bounded only by the expiry claim, and §4 names the resulting window in which a revoked token still works. You do not own those caches, and in a platform with third-party extensions you may not know how many there are.
  4. Resource servers that validate locally are consulting nothing. Per RFC 9068 §4 the required check is that the current time precedes the expiry claim. There is no step in that validation at which your revocation could be observed.
  5. On AWS, revoking sessions attaches an inline policy named AWSRevokeOlderSessions to the role, denying access to anyone who assumed it in the past and approximately thirty seconds into the future — a deliberate margin, documented as accounting for policy propagation so that a session acquired or renewed just before the update is still caught. The condition is a DateLessThan comparison on the token issue time. This is a time-conditioned deny evaluated at the authorization boundary, not a credential being destroyed. And the client side is separate: the AWS CLI caches credentials until they expire, so the cache directory has to be cleared by hand.
  6. On Azure, the back-end services for managed identities maintain a cache per resource URI for around twenty-four hours. Microsoft's own guidance states that changes to a managed identity's group or role membership can take several hours to take effect, and that it is not currently possible to force a managed identity's token to be refreshed before its expiry. An authorization change you make now is a change that takes effect at a time you cannot choose.
  7. On Google Cloud, the workload identity pool — the federation container itself — is soft-deleted and can be undeleted for up to thirty days, after which deletion is permanent, and deleting a pool also deletes its providers, with the documented consequence that identities in the pool lose access to Google Cloud resources. What the documentation does not state is the timing: whether already-issued access tokens stop working at the moment of deletion or continue to function until they expire. I am not going to assert either behaviour, because the page does not say. The absence of documented deprovisioning semantics for a federation container is itself the finding.

Add those up and the honest description of the state of the art is this: every substrate documents a lower bound on the delay between deciding to revoke and the credential actually being refused, and not one of them documents an upper bound. The AWS margin is thirty seconds plus client cache. The Azure figure is around twenty-four hours for the authorization cache. The RFC propagation delay is conceded without a number. The introspection window is bounded by an expiry the revoking party does not control. These are floors, published for good engineering reasons, and treating them as ceilings is the error that turns an incident-response plan into a document.

The practical consequence for a platform team is narrow and testable. If you have never measured the interval between calling revocation and observing an actual refusal at a real resource server, you do not know whether your revocation path works, and the specification has told you in advance that the response code will not tell you either. That measurement is a few hundred lines of code and an hour of somebody's afternoon. Very few estates have it.

What “revoked” means, on the clock Each bar runs from the moment revocation is decided to the moment the credential is actually refused. DECISION ACTUAL REFUSAL Revocation endpoint returns 200 200 is also the answer for an invalid token The grant behind the token SHOULD / MAY — cascading is optional Propagation across servers conceded in the spec, no bound given Cached introspection response bounded only by the expiry claim Self-contained JWT, validated locally expiry is the only required check AWS role session deny policy, ~30s forward, plus client cache Azure managed identity ~24h cache; no forced refresh before expiry None of these bars has a documented right-hand end. Every published figure here is a lower bound on the delay, not a bound on it. Two of the seven are explicitly open: the propagation delay is conceded without a number, and the cached-introspection window is bounded only by an expiry the revoking party does not control. A revocation you have never measured is indistinguishable from one that does nothing. Sources: RFC 7009 sections 2.1, 2.2 and 5 · RFC 7662 sections 2.2 and 4 · RFC 9068 sections 2.2 and 4 · AWS IAM User Guide, revoking temporary security credentials · Microsoft Entra managed-identity best practices.
Configuration

Three measurements this argument implies

The claims above are testable, and a teardown that names a defect without naming the measurement that would settle it is just an opinion with citations. These are the three probes. None of them fixes anything — the remedy is the companion piece — but each converts a belief into a number.

The first defect is definitional, so the first fix is a type. This module makes it impossible to report a machine-identity count without declaring the counting unit, and impossible to compare or ratio two counts whose units differ. It is deliberately unhelpful: the compiler will not let you produce the single reassuring number that everyone wants.

identity-census.ts
/**
 * A machine-identity census that will not produce a scalar.
 *
 * Every observation must declare which object it counted. The units below are the
 * ones the platforms' own documentation makes countable; they are not
 * interchangeable, and comparing across them is a type error rather than a
 * judgement call.
 */

export type CountingUnit =
  /** A principal object in a directory, excluding anything soft-deleted. */
  | "directory-principal-live"
  /** The same, including soft-deleted objects still inside their purge window.
   *  Azure counts system-assigned identities here for 30 days after deletion. */
  | "directory-principal-including-soft-deleted"
  /** Temporary session credentials with no corresponding stored object. */
  | "role-session"
  /** Distinct credentials issued over a window. Under short-TTL regimes this is
   *  a flow rate, not a population, and MUST carry a window. */
  | "credential-issued"
  /** Credentials whose expiry has not yet passed, at an instant. */
  | "credential-live"
  /** Occurrences of a secret in source control. Not an identity count. */
  | "secret-occurrence";

/** Units whose value is meaningless without the observation window. */
const FLOW_UNITS: ReadonlySet<CountingUnit> = new Set<CountingUnit>([
  "credential-issued",
]);

export type Observation = {
  readonly unit: CountingUnit;
  /** The system that produced the number, not the vendor that published it. */
  readonly source: string;
  readonly count: number;
  /** ISO-8601 instant at which the count was taken. */
  readonly observedAt: string;
  /** ISO-8601 duration, required for flow units. */
  readonly window?: string;
  /** What this number is known to exclude. Empty is a claim, not a default. */
  readonly knownExclusions: readonly string[];
};

export class UnitMismatchError extends Error {
  constructor(a: CountingUnit, b: CountingUnit) {
    super(
      `Refusing to combine counts of different objects: "${a}" and "${b}". ` +
        "These are different quantities sharing a label.",
    );
    this.name = "UnitMismatchError";
  }
}

export function validate(observation: Observation): readonly string[] {
  const problems: string[] = [];
  if (!Number.isInteger(observation.count) || observation.count < 0) {
    problems.push("count must be a non-negative integer");
  }
  if (Number.isNaN(Date.parse(observation.observedAt))) {
    problems.push("observedAt must be an ISO-8601 instant");
  }
  if (FLOW_UNITS.has(observation.unit) && !observation.window) {
    problems.push(`unit "${observation.unit}" is a rate and requires a window`);
  }
  if (observation.knownExclusions.length === 0) {
    problems.push(
      "knownExclusions is empty — state what the number excludes, or say 'none verified'",
    );
  }
  return problems;
}

/** A ratio is only defined between observations of the same object. */
export function ratio(numerator: Observation, denominator: Observation): number {
  if (numerator.unit !== denominator.unit) {
    throw new UnitMismatchError(numerator.unit, denominator.unit);
  }
  if (denominator.count === 0) {
    throw new Error("denominator is zero");
  }
  return numerator.count / denominator.count;
}

/** The deliverable is a table with units attached, never a headline figure. */
export function report(observations: readonly Observation[]): string {
  return observations
    .map((o) => {
      const issues = validate(o);
      const suffix = issues.length > 0 ? `  [UNSOUND: ${issues.join("; ")}]` : "";
      const win = o.window ? ` per ${o.window}` : "";
      return `${o.count}${win}  ${o.unit}  (${o.source}, ${o.observedAt})${suffix}`;
    })
    .join("\n");
}

These probes measure; they do not remediate. Deleting an orphaned grant on the strength of a single lookup is how a resolver outage becomes an outage of your own — which is why the indeterminate case is a first-class return value rather than an error to swallow.

The grant that outlives its principal

One case deserves separating out, because it is the sharpest available demonstration that this is a modelling gap rather than a hygiene gap, and because a platform vendor documents it against its own product with admirable directness.

Microsoft's guidance for managed identities states that role assignments are not automatically deleted when either a system-assigned or a user-assigned managed identity is deleted, and that these assignments should be deleted manually. Orphaned assignments then appear in the portal as “Identity not found”, with an ObjectType value of Unknown.

Sit with what that object is. It is an authorisation — a grant of a specific role over a specific scope — whose subject cannot be resolved. It is not expired; expiry would be a defined state with defined consequences. It is not revoked; revocation would be a decision somebody made and could be asked about. Its principal is Unknown, and Unknown here is not a placeholder for a value someone forgot to fill in. It is the system telling you, accurately, that it cannot characterise its own state.

The security consequence is real but secondary — the identifier space is large, and reuse is not the primary worry. The primary worry is what it does to the two questions that matter. “Who can reach this scope” now has an answer containing entries that resolve to nothing, so the answer is incomplete in a direction you cannot bound. And “who granted this” has no answer at all, because the grant records a principal rather than a grantor, and the principal is the thing that vanished. This is the human lifecycle's assumption failing in the open: in a joiner-mover-leaver process, the identity is durable and externally authoritative, so hanging a grant off it is sound modelling. When the principal is a workload that lived for ninety seconds, the same modelling produces a dangling reference and calls it a role assignment.

What the field already knows, and what it solved instead

None of this is a gap in anybody's competence, and the surrounding work is genuinely strong — which is why the specific shape of what is missing is worth stating carefully.

SPIFFE is the closest thing the field has to a correct answer at the credential layer, and it is instructive precisely because of how much it gets right. A SPIFFE ID is a constrained URI — an RFC 3986 compliant URI comprising a trust domain name and a path, of the form spiffe://trust-domain-name/path — with the scheme fixed, a non-empty lowercase host, query and fragment components excluded, and implementations required to support IDs up to 2048 bytes. That is a real naming discipline rather than a convention, and it is machine-checkable. The X.509 SVID standard then does something that ought to be much more widely copied: it requires that an SVID contain exactly one URI SAN and therefore exactly one SPIFFE ID, and requires that validators encountering more than one URI SAN reject the SVID outright. Ambiguity fails closed. It further requires that leaf SVIDs have a non-root path component, and that the key usage extension be present and marked critical.

That is the shape of the answer at the credential layer: one credential, one identity, ambiguity rejected rather than resolved by guesswork. What a trust-domain path is not, however, is a registry. It tells you how to name a workload; it does not tell you which names are currently in use, which workload instance holds each one, who authorised the workload to exist, or what happens to the name when the workload does not. Naming discipline and population accounting are different jobs, and the field has done the first one well.

The mandatory field set is further along than the conversation usually admits. RFC 9068 already requires seven claims in a JWT access token — issuer, expiry, audience, subject, client identifier, issued-at, and a unique token identifier. Issuer, principal, audience, expiry and a unique credential identifier are not aspirational governance metadata that somebody would have to invent; they are already mandatory in the profile that a large share of the industry's machine credentials follow. What is missing from that list is not a field so much as a referent: nothing in it points at a durable record of the workload, and the unique token identifier identifies the credential rather than the thing holding it.

The evidence on how long credentials persist once they escape is the part I would put in front of a sceptical platform executive, because it is longitudinal and it is published by the party that measured it. GitGuardian reports that nearly 70 per cent of credentials confirmed valid in 2022 were still valid in January 2025, and that 64 per cent remained valid when retested in January 2026 — meaning they had not been remediated. That is a measurement of credential validity over time rather than an identity count, and it should not be pressed into service as one. But as a statement about the reverse path it is devastating: publicly exposed, known, findable credentials are still working three to four years later. If the deprovisioning path fails at that rate for credentials somebody has actually told you about, no reasonable prior says it succeeds for the ones nobody knows exist.

Entro's age distribution points the same way from a different angle: 7.5 per cent of machine identities in cloud environments are between five and ten years old, and over 2 per cent of active secrets are more than a decade old. A decade-old active secret is not a credential in a lifecycle. It is a credential in a sediment layer.

As for who will specify the controls: on the supervisory side in North America, the revised interagency model risk management guidance of 17 April 2026 — OCC Bulletin 2026-13, with the parallel Federal Reserve issuance SR 26-2 — supersedes SR 11-7 and SR 21-8, states that it does not set forth enforceable standards or prescriptive requirements, and in the OCC's own wording places generative and agentic AI models outside its scope as novel and rapidly evolving, with separate guidance promised. That is a deferral rather than an exemption: every obligation attached to the underlying action is untouched, and what was removed is the framework that would have specified the controls. The practical reading for a platform team is narrow and worth taking seriously — the separate guidance, when it arrives, will be written against whatever the industry has already built. The same holds in India and across the Gulf, where supervisory attention to AI systems is active and the institutions your customers serve will be asked who authorised a given action long before anyone publishes a control specification. Nobody is going to hand a platform an agent-credential specification in time for it to be a requirement rather than a choice.

Where this argument is weakest

An argument that cannot say what would refute it is not worth the citations attached to it, so here is what would.

A platform that can already answer. If a platform can produce, on demand and without a reconstruction pipeline, a complete list of live machine identities with a stable identifier, the workload each is bound to, an accountable owner, and an expiry — then my argument is simply wrong for that platform. This is directly testable and I would encourage anyone reading to test it on their own product before agreeing with me. Ask for the list, unannounced, with a clock running. The prediction under test is that the answer arrives as a query somebody writes rather than a record somebody holds, and that two competent engineers running it independently return different numbers. I have not run that experiment across platforms and I am not going to present it as though I had.

The population may be the wrong object entirely. The strongest version of the counter-argument is not that counting is hard but that counting is the wrong ambition. What matters for risk is not how many identities exist but what the reachable capability graph looks like — which principals can, in composition, reach which consequential actions. On that view a registry of identities is a comforting artefact that answers a question nobody should be asking, and the effort should go into authorisation rather than enumeration. I think that view is substantially right about what matters and wrong about sequencing, since a capability graph is computed over a set of principals and inherits every gap in it. But it is a serious position and I would not pretend it has been disposed of by anything above.

Ephemerality may already be a complete answer. If credentials live for one hour, are sender-constrained so a stolen one is useless off its channel, and are issued only against an attested workload, then a stranded credential is a five-minute problem and the accumulation I have described is an accounting curiosity rather than a risk. In a well-run SPIFFE mesh this is close to true today. My response is that it holds exactly where the mesh's assumptions hold, and agent platforms routinely issue credentials to things outside any mesh — third-party tool integrations, customer-supplied connectors, extensions running in someone else's tenancy. The argument is strongest at the ragged edge of the platform and weakest at its well-governed centre, and I should say so rather than generalise from the edge.

My scale evidence is thin, and I would not present it as more. The ratio figure I cited is vendor telemetry from a self-selected customer base, published without a stated counting unit. It is not a census and cannot be treated as one. The GitGuardian figures are carefully scoped but measure secrets in public commits, a population chosen for observability rather than representativeness. I have deliberately not repeated the other ratios in circulation because I did not read them at source. What survives all of that is not a quantity but a structure: the argument here rests on the asymmetry between issuance and deprovisioning and on the specifications' own text about revocation, neither of which depends on any ratio being right.

The cost may not be worth paying. A durable identity record per issued workload identity is not free. It is storage proportional to issuance rate, a write on the critical path or a queue that can lag, a new consistency problem between the record and the credential, a new deletion obligation under data-protection regimes in several of the markets that matter, and a new thing that can be down when issuance is not. A platform team could look at that honestly and conclude the cost exceeds the benefit at their scale. That is a legitimate engineering conclusion. What is not legitimate is arriving at it by default, without anyone having framed it as a decision.

The product decision that was never framed as one

Everything above converges on a single point about where this sits organisationally, and it is the reason I think this argument belongs in front of platform engineers rather than in front of the customers currently struggling with it.

Every customer answering an authority question about agents — who authorised this action, what could this identity reach, prove this credential no longer works — is reconstructing an answer from primitives the platform chose to emit. They are folding an event stream into a state, on join keys that do not exist, against a definition of the countable object that has never been published, and they are doing it thousands of times over, independently, with different results. Some of them will get it wrong in the reassuring direction, which is worse than getting it wrong in the alarming one.

The choice of which primitives to emit is a product decision. It has simply never been framed as one, because it was made incrementally, by the people who built the issuance path, under entirely correct pressure to make issuance fast and reliable, at a time when the issuance rate was low enough that the reverse path did not matter. Every individual decision along the way was right. The aggregate is a substrate that makes a question its customers must answer either expensive or unanswerable, and which of those two it is currently depends on the customer's engineering budget.

The platform is also the only party positioned to change it. A customer cannot retrofit a durable identity record onto credentials they did not issue; they can only observe issuance events after the fact and guess at the joins. They cannot make a revocation propagate faster than the platform's caches allow, and they cannot discover the caches they do not own. They cannot define the counting unit, because the object being counted is defined by the platform's own data model. Every one of these is upstream of the customer, and every one of them is downstream of a decision someone on a platform team could take deliberately.

I have deliberately not proposed the remedy here. The obvious sketch — a durable identity record emitted at issuance, a stable key joining credential to workload to owner, a published counting unit, a revocation plane whose latency is measured rather than asserted — raises hard questions that a teardown cannot answer responsibly: what the record costs at issuance rate, how it stays consistent with the credential without becoming a second source of truth, what its own retention and deletion obligations are, whether revocation belongs in the data plane or the control plane, and how any of it degrades when the registry is unavailable and issuance must continue. Those belong in a design, with the trade-offs argued at full length and the failure modes named. That is the companion piece — A registry and revocation plane for platform-issued agent credentials — and it is forthcoming.

What this piece claims is narrower and, I think, harder to dismiss. The rate at which identities are being issued has changed by orders of magnitude. The lifecycle receiving them was designed around a durable human record, arriving at human rate, from an authoritative external system, and none of those three properties survives contact with an agent runtime. The residue is not carelessness; it is what a loud forward path and a silent reverse path compute over a year. And the number everybody quotes to describe the problem cannot be checked, because there is no agreed definition of the thing being counted — a fact that is not a footnote about measurement hygiene but the clearest available evidence that the object itself was never modelled.

If you build one of these platforms, the useful next hour is not a strategy discussion. It is asking your own system for the list, timing how long the answer takes and how confident anybody is in it, and then running the revocation probe against a disposable identity to find out what your revocation actually does. Both results are cheap to obtain and neither is currently known, which is itself the finding.