The companion teardown on this site — “Every engagement leaves credentials behind and no engagement is scoped to collect them” — argues that this is structural rather than a matter of diligence. A delivery team creates identities because it cannot build anything otherwise; the statement of work enumerates systems, milestones and acceptance criteria; nothing in it enumerates the identities, so nothing at closedown collects them. I am not going to re-argue that. This piece assumes it and answers the question that follows, which is the one an engagement lead actually has to answer on a Monday: what do you build, precisely, inside a client's estate you do not control, and what does it cost.

The scope is narrow on purpose. Not an identity governance programme, not a discovery tool that crawls a client estate and reconciles what it finds. A register of the credentials your engagement creates, populated at the moment of creation, with ownership transfer as an explicit signed step at handover. That boundary is doing real work: the credentials that existed before you arrived are not yours to fix and pretending otherwise is how this becomes a twelve-month programme nobody bought. What you created, you can account for. That is the whole claim.

Two constraints rule out most of what you would build first. It has to be implementable inside a client's existing estate without a platform migration — you are a guest, you do not get to replace their identity provider, and any design whose first step is “stand up a secrets platform” is a design that will be descoped in week three. And it has to produce something that survives the team leaving. A register hosted in your firm's tooling, backed by your firm's runbooks, accessible through your firm's single sign-on, is an asset that walks out of the building with the last consultant. Both constraints point the same direction, and what survives them is smaller than you would expect.

What has to be true before anything else

Start from the premise and follow it without shortcuts, because most of what follows is forced, and it is worth being able to see which parts are genuinely 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 nobody to ask whether the thing is still needed, so nobody ever asks, so it is never removed. Without a tested revocation path, the ability to remove it is a belief rather than a capability — and beliefs about revocation are unusually badly calibrated, for reasons the specifications themselves are candid about.

“Tested” is not a rhetorical flourish. RFC 7009, the OAuth token revocation specification, is explicit that the response code tells you almost nothing: the authorization server returns a success status both when a token has been revoked and when the client submitted an invalid token, so the code cannot distinguish a working endpoint from an endpoint that did nothing to a token that was garbage in the first place. Section 2.1 states that invalidation takes place immediately, and then concedes in the next breath that in practice there could be a propagation delay in which some servers know about the invalidation while others do not. Section 3 goes further: where access tokens are self-contained, some currently non-standardized backend interaction between the authorization server and the resource server may be needed if immediate revocation is desired. The standard describes an operation, and then tells you it has not standardised the part that makes the operation bite at the place the credential is actually used.

The other half of that gap is in the token profile. RFC 9068, the JWT profile for OAuth 2.0 access tokens, enumerates what a resource server must validate: the header type, the issuer, the audience, the signature, and that the current time is before the time represented by the expiry claim. It specifies no revocation check and no introspection call. A conforming resource server that accepts a revoked-but-unexpired token is not misbehaving. It is doing exactly what the specification told it to do. So the only evidence that a revocation path works is an observation, at a real resource server, that a credential which used to be accepted has stopped being accepted. Everything short of that is an assertion with a URL attached.

From the premise, the first consequence: the record has to exist at the moment the credential does. Not at closedown, not at the next audit. This is not a preference about tidiness — it is an argument about who is in the room. At the moment a credential is created, exactly one party knows why it exists, which deliverable required it, which workload will hold it and who on the delivery team is answerable for it: the person creating it. Six months later that person is on another account and the information is not recoverable from the artefact. You cannot read purpose off an IAM role. You cannot read the owner off a client secret. A record written at creation costs one line in a module; the same record reconstructed at closedown costs an archaeology exercise nobody scoped and nobody funded, and it produces guesses.

Second consequence: if the record has to exist at creation, something has to refuse when it does not. The refusal has to be mechanical rather than procedural, because a procedural gate is a checklist item and checklist items are the first thing to go when a delivery date moves. In a client estate you cannot put that refusal at the signing key — you do not hold it. But you do hold something almost as good: the engagement creates its credentials through a small number of paths that the engagement itself built. The infrastructure module. The pipeline job. The one console runbook everybody follows. Those are yours to wrap, and wrapping them is a day of work rather than a platform.

Third: the record must be total. A partial record is worse than none, 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. A client told there are roughly two hundred service identities in the estate convenes a meeting. A client told there are one hundred and forty defined records and sixty undefined ones, each with a named missing field, has a work queue with sixty items in it and a specific person to ask about each.

Fourth, and this is the part specific to a services firm: the handover is where the design either becomes real or becomes a slide. An identity whose owner is a consultant is fine during delivery and is a liability the day the consultant rolls off. So ownership is not a string on a record; it is a binding that carries a side — firm or client — and a transfer that has to be accepted by a named client principal before the engagement can close. That single field is what converts a register from documentation into an acceptance condition.

FIGURE 1 · THE UNIT OF RECORD The seven fields, and what actually requires them One instrument requires the owner field. One requires removal, and ties the trigger to a person leaving. The rest of the record is named by nothing that governs the engagement. THE RECORD issuer which system minted it NAMED BY NO INSTRUMENT principal which workload holds it CSF 2.0 PR.AA-01 outcome stated, no control specified material the key, and its unique id NAMED BY NO INSTRUMENT owner the human answerable for it SP 800-53 AC-2(b) “assign account managers” purpose the class of action it exists for NAMED BY NO INSTRUMENT expiry the moment it stops working NAMED BY NO INSTRUMENT revocation endpoint, and the last drill date SP 800-53 AC-2(f), AC-2(l) removal trigger is a person leaving THE RULE A record missing any field is not partial. It is undefined. Undefined fails closed, and cannot be handed over. Written at creation, during delivery. Never reconstructed at closedown. WHY IT IS THE FIRM’S PROBLEM The firm is the only party present at the moment each credential is made. A record written then costs a line. The same record reconstructed later costs an archaeology exercise that nobody scoped and nobody funded. WHAT THE GOVERNING INSTRUMENTS ACTUALLY SAY 88 FR 37920 · TERMINATION Provides for the return or destruction of the organisation’s “data, information, and other resources” at exit. Not credentials. Not keys. Not the accounts you created. SP 800-53 PS-7 · PROVIDERS The provider must notify the client of transfers or terminations of external personnel holding credentials or privileges. There is no parallel control for a machine credential it created. CSF 2.0 · GV.SC-10, GV.SC-05 Plans must cover activities after a partnership ends; requirements must be integrated into supplier contracts. Neither names an identity, a credential, or a revocation test. A reading of four named documents, not a survey of the field. Quoted wording is each publisher’s own.

The instruments name people, and the removal trigger is a resignation

The obvious objection to all of this is that it is somebody else's job — that identity governance is a client function, that a mature client will already have a policy, and that a delivery firm inventing its own register is scope creep dressed as rigour. That objection deserves to be taken at full strength, because it is usually made by people who have read more of the control catalogues than the person answering it. So here is what those catalogues actually say, read directly.

Start with the document most often handed over as the authority on identity assurance. NIST SP 800-63-4, the current revision of the digital identity guidelines, states in its Scope and Applicability section that for that publication, “person” refers only to natural persons, and that the guidelines do not explicitly address some subjects including, but not limited to, machine-to-machine authentication, interconnected devices such as Internet of Things devices, or access to Application Programming Interfaces on behalf of subjects. The document everybody cites when they say “we follow NIST on identity” says, in its own words, that the objects in dispute are outside its scope. It is not that it takes a weak position on machine credentials. It declines to take one.

Then the control catalogue. NIST SP 800-53 Rev. 5 never uses the term “service account”; a full-text search of the published catalogue returns no occurrences of the phrase. The word “service” appears as an account type exactly once, inside the non-normative Discussion under AC-2, in a list of examples: individual, shared, group, system, guest, anonymous, emergency, developer, temporary, and service. No distinct lifecycle requirement attaches to that type anywhere in the catalogue. I want to be careful about what I am claiming here, because the tempting version of this claim is much bigger than the evidence: I am not saying no standards body anywhere defines a machine identity. I am saying that the two documents a services firm is most likely to be handed as the identity authority for an engagement do not describe the object, and both of them say so in their own text.

AC-2 is not silent, though, and it is worth being precise about what it does require, because it requires two of the seven fields this design argues for. It requires that the organisation assign account managers — that is the owner field, mandated outright. And it requires that accounts be created, enabled, modified, disabled and removed in accordance with organisation-defined policy. Then it closes with the item that matters most here: align account management processes with personnel termination and transfer processes. The owner field is required, and the trigger that fires deprovisioning is a person leaving. Nothing in that control fires when a workload is decommissioned, a pilot ends, or a delivery team rolls off.

The pattern repeats in the one control that places a duty on an outside services provider. PS-7, external personnel security, requires organisations to make external providers notify them of any personnel transfers or terminations of external personnel who possess organisational credentials or badges, or who have system privileges, within a defined period. Its Discussion names service bureaus, contractors, and other organisations that provide system development and information technology services as the providers in view — which is to say, it is aimed squarely at firms like the reader's. And it covers people. There is no parallel control requiring a provider to notify the client when a machine credential the provider created is no longer needed. The obligation to tell the client that a consultant has left exists. The obligation to tell them that the service principal the consultant built is now orphaned does not.

NIST CSF 2.0 is the instrument that does name the object as first-class. Subcategory PR.AA-01 reads that identities and credentials for authorized users, services, and hardware are managed by the organization, and PR.AA-03 that users, services, and hardware are authenticated. Services are in the text, unambiguously. But the same document states that the framework does not prescribe how outcomes should be achieved — so it establishes an obligation and specifies no control, which leaves the whole of the how to the contract. It also carries the single most useful hook a services firm has: GV.SC-10 requires that cybersecurity supply chain risk management plans include provisions for activities that occur after the conclusion of a partnership or service agreement, and GV.SC-05 requires supply chain security requirements to be established, prioritised and integrated into contracts and other agreements with suppliers. Those two are the lines a statement of work can be measured against — and neither of them names an identity, a credential, or a revocation test.

The US interagency third-party guidance closes the loop the same way. Under Default and Termination it advises contractual provisions that provide for the timely return or destruction of the banking organization's data, information, and other resources. Data, information, resources. Not credentials, not keys, not accounts, not the service principals your engagement created. What is striking is that the same guidance takes systems access extremely seriously at the front of the relationship: due diligence includes understanding potential information security implications, including access to the banking organization's systems and to its confidential information, and the confidentiality section notes increased risks related to the sensitivity of non-public information or access to infrastructure. Access is scoped carefully at deal time. Its withdrawal is not scoped at exit.

And the guidance is explicitly non-binding and explicitly non-delegating, which is exactly why the contract is the live instrument rather than the regulation. It states that supervisory guidance does not have the force and effect of law and does not impose any new requirements, while holding that a banking organization's use of third parties does not diminish its responsibility to meet those requirements. The duty stays with the client; the guidance specifies no controls; so whatever comes back at the end is whatever the statement of work said would come back. It is also worth noting what the guidance does say about the delivery model most services firms actually run: under Subcontracting it advises addressing when and how the third party should notify the client of its use or intent to use a subcontractor, whether specific subcontractors are prohibited, and whether the contract should prohibit assignment, transfer or subcontracting without consent — and where subcontracting is integral to the activity it advises more detailed contractual obligations, such as reporting on the subcontractor’s conformance with performance measures and periodic audit results. (The list of subcontractor risks often quoted alongside this — access to the client’s infrastructure, extended chains — sits in the preamble as a summary of commenters’ views; the agencies responded by narrowing the guidance to the third party’s own oversight processes.) Multi-tier delivery is named. What the tiers create is not.

Outside North America the picture is more prescriptive in one market and thinner in another, and both matter for firms delivering across them. India's binding instrument is the Reserve Bank of India's Outsourcing of Information Technology Services Directions, 2023, and it is considerably more directive than the US guidance: it sets minimum contents for the outsourcing agreement including effective access by the regulated entity to data, books, records, information, logs and business premises; it requires prior consent for sub-contracting and holds the provider contractually liable for its sub-contractors; it extends the right of audit to those sub-contractors; and its exit strategy chapter requires safe removal or destruction of data, hardware and records, with the provider prohibited from erasing anything during the transition period. Clause numbering should be re-verified against the current Master Direction before anyone quotes it into a contract, and I have deliberately paraphrased rather than quoted for that reason. In the Gulf, Saudi Arabia’s SAMA Cyber Security Framework is, of the instruments read for this piece, the only one that names non-human privileged accounts directly, requiring under its identity and access management domain that non-personal privileged accounts be limited and monitored, with periodic review of privileged accounts and a maintained audit trail of access requests, approvals and revocations — though the Rulebook page carries a soft-launch label and an old date, so the effective version needs confirming before it is cited into anything binding.

One further note for firms building agentic systems in regulated estates, because it is routinely read backwards. 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 — changed 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. That is a deferral, not an exemption. Every obligation attached to the underlying action survives untouched; what was withdrawn is the framework that would have specified the controls. For a services firm the implication is uncomfortable and specific: when the separate guidance arrives, it will be written against whatever the industry has already built, and the firms that can show which identities their engagements created and demonstrate that those identities can be taken away will find their practice described rather than corrected.

So the answer to the objection is not that the client is negligent. It is that three instruments establish a post-engagement handback duty, none of them names an identity, one control catalogue requires an owner and fires removal on a resignation, and the identity guideline excludes the object from its scope in the first section. That absence is the finding. It should not be filled in with an invented clause — I looked for a client-side master services agreement or acceptance template that requires enumeration of every identity created during an engagement and a tested revocation path as an acceptance condition, and I did not find one from a primary publisher. The honest thing to do with a gap is to build into it, not to claim somebody already wrote the rule.

The unit, field by field

Seven fields. Each one is here because leaving it out breaks something specific, and it is worth walking the derivation rather than presenting the schema as a fait accompli.

Issuer and principal. Which system minted the credential, and which workload holds it. These are the fields that make the record joinable to reality: without the issuer you cannot find the credential to revoke it, and without the principal you cannot tell whether the thing it belongs to still exists. The principal is also the field that decays first and most invisibly, because workloads are deleted far more casually than accounts are. A record whose principal no longer resolves is not an error condition to be swallowed — it is exactly the population the whole exercise exists to surface.

Material. The class of credential and its identifier — an OAuth client, a static secret in a named store with a rotation interval, a certificate serial, a cloud role resource identifier. The class matters more than it looks, because revocation semantics differ per class in ways that are not interchangeable, and a register that flattens them into “credential” will produce a runbook that does not work. Kubernetes is the cleanest illustration: the project's own documentation says projected and TokenRequest service account tokens expire automatically and rotate, while Secret-based service account tokens are not recommended precisely because they do not expire and do not rotate. Same nominal object, opposite lifecycle properties, and any engagement that left the second kind behind left a credential with nothing to fail closed on.

Owner, carrying a side. A directory principal that the register re-resolves on a schedule, not a name typed into a spreadsheet. It needs its own resolution expiry: past that moment the record returns to undefined until re-resolution succeeds, which bounds the decay to the schedule interval instead of letting it run unbounded. And it carries the field that makes this a services design rather than a governance design — the side. An owner on the firm's side is legitimate during delivery and is a defect at handover, and having that be a typed distinction rather than a convention is what lets an acceptance gate check it mechanically.

Purpose, as a reference rather than a sentence. Free-text purpose is unfalsifiable, and an unfalsifiable field is decoration with a maintenance cost attached. The workable form is a reference into a small register of action classes the client owns, plus the statement of work line item that caused the credential to exist. That second half is the one people skip and it is the one that pays: a credential traceable to a deliverable can be retired when the deliverable is retired, which is the closest thing to a decommissioning trigger this problem has. The framework's removal trigger fires when a person leaves; a deliverable reference gives you one that fires when the work ends.

Expiry. The moment it stops working on its own. Three shapes are real: it expires, it rotates on an interval, or it does neither. The third is representable in the type because it exists in the wild in enormous numbers, and it is never a defined record. That is a deliberately harsh rule and it will be argued with, because plenty of production systems depend on a non-expiring credential. The argument is worth having explicitly at creation time rather than implicitly at incident time.

Revocation. Not a URL. A method, a target, a runbook reference naming who can actually execute it, and the date a drill last observed a real refusal through that binding. If that last element is null, the binding is undefined, and the record it is attached to is undefined with it. The premise said “tested revocation path”; this field is what turns the word “tested” into something a program can check rather than an adjective in a closing report.

One design decision inside that last field is worth naming because it changes the cost materially. The drill runs against the binding, not against each individual credential. A binding is a method plus a target — the OAuth revocation endpoint of a particular authorisation server, the deny-policy path on a particular cloud account, the key-deletion runbook for a particular vault mount. Thirty credentials might share four bindings. Drilling four bindings is a weekly job; drilling thirty credentials is a project nobody will fund, and a design that demanded it would be quietly disabled by March.

The data model

What follows is the core of the reference package, written against generic cloud and directory interfaces because that is the shape of most estates. The strict typing is load-bearing rather than stylistic: every field on the record is required, so 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 somewhere it cannot be forgotten under delivery pressure.

Configuration

@authority/nhi — record, creation hook, drill, handover

Four modules. The first defines what a credential record is and when it is undefined. The second wraps whatever the engagement already uses to create credentials, so nothing requires a new issuance path. The third is the drill that produces the number. The fourth is the handover manifest, which is the deliverable.

The unit of record as a total type. Note what is not representable: an owner with no directory identity, a purpose with no action class, a revocation binding with no runbook. Note also what deliberately is representable — a credential with no expiry — because that shape exists in every estate and the design's job is to classify it, not to pretend it away.

packages/nhi/src/record.ts
// @authority/nhi — the unit of record for a credential created during an engagement.
//
// 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 EngagementId = Brand<string, "EngagementId">;
export type CredentialId = Brand<string, "CredentialId">;
export type UnixSeconds = Brand<number, "UnixSeconds">;

/** Which system minted it. Without this you cannot find the thing in order to remove it. */
export type IssuerRef =
  | { readonly kind: "cloud-iam"; readonly provider: "aws" | "azure" | "gcp"; readonly scope: string }
  | { readonly kind: "cluster-service-account"; readonly cluster: string; readonly namespace: string }
  | { readonly kind: "directory-application"; readonly tenant: string }
  | { readonly kind: "saas-admin"; readonly vendor: string; readonly tenant: string }
  | { readonly kind: "vault"; readonly mount: string };

/** The class matters because revocation semantics differ per class and are not interchangeable.
 *  Kubernetes is the clean case: the project documents that projected and TokenRequest tokens
 *  expire and rotate, while Secret-mounted service account tokens do neither. */
export type CredentialMaterial =
  | { readonly kind: "oauth-client"; readonly clientId: string; readonly senderConstrained: boolean }
  | { readonly kind: "static-secret"; readonly storeRef: string }
  | { readonly kind: "x509"; readonly serialNumber: string }
  | { readonly kind: "cloud-role"; readonly resourceId: string }
  | { readonly kind: "cluster-token"; readonly projected: boolean };

/** A directory principal that must still resolve — not a name typed into a form. The side field
 *  is what makes this a services design: an owner on the firm's side is fine during delivery and
 *  is a defect at handover, and an acceptance gate can check that mechanically. */
export interface OwnerBinding {
  readonly kind: "person" | "group";
  readonly directoryId: string;
  readonly side: "firm" | "client";
  readonly resolvedAt: UnixSeconds;
  readonly resolutionExpiresAt: UnixSeconds;
}

/** actionClassRef points into the client's own small register of action classes. deliverableRef
 *  points at the statement-of-work line that caused this credential to exist — which is the
 *  closest thing this problem has to a decommissioning trigger, since the control catalogues
 *  fire removal on a person leaving and never on work ending. */
export interface PurposeBinding {
  readonly actionClassRef: string;
  readonly deliverableRef: string;
  readonly statement: string;
}

export type RevocationMethod =
  | "oauth-revocation-endpoint"
  | "iam-policy-deny"
  | "key-deletion"
  | "console-runbook";

/** A binding is a method plus a target. Many credentials share one binding, and the drill runs
 *  per binding rather than per credential — otherwise the drill is a project nobody funds. */
export interface RevocationBinding {
  readonly method: RevocationMethod;
  readonly target: string;
  /** Who can actually execute this at 03:00. A method with no named runbook is a wish. */
  readonly runbookRef: string;
}

export function bindingKey(binding: RevocationBinding): string {
  return binding.method + "::" + binding.target;
}

/** "none" is representable because it exists everywhere in the wild. It is never DEFINED. */
export type Expiry =
  | { readonly kind: "expires"; readonly at: UnixSeconds }
  | { readonly kind: "rotates"; readonly everySeconds: number; readonly nextRotationAt: UnixSeconds }
  | { readonly kind: "none" };

export interface CredentialRecord {
  readonly credentialId: CredentialId;
  readonly engagementId: EngagementId;
  readonly issuer: IssuerRef;
  readonly principal: string;
  readonly material: CredentialMaterial;
  readonly owner: OwnerBinding;
  readonly purpose: PurposeBinding;
  readonly expiry: Expiry;
  readonly revocation: RevocationBinding;
  readonly createdAt: UnixSeconds;
  readonly createdBy: string;
}

/** The outcome of one drill against one binding. "still-accepted-at-timeout" is a real result
 *  and must be storable: a drill that can only record success is a drill that measures nothing. */
export interface DrillOutcome {
  readonly bindingKey: string;
  readonly ranAt: UnixSeconds;
  readonly result: "refused" | "still-accepted-at-timeout";
  readonly observedIntervalSeconds: number;
}

export interface Deficiency {
  readonly field: string;
  readonly why: string;
}

export type Definition =
  | { readonly status: "defined"; readonly record: CredentialRecord }
  | { readonly status: "undefined"; readonly record: CredentialRecord; readonly deficiencies: readonly Deficiency[] };

export interface ClassifyContext {
  readonly now: UnixSeconds;
  /** Most recent drill for this record's binding, or null if one has never run. */
  readonly drill: DrillOutcome | null;
  readonly drillFreshnessSeconds: number;
}

/** The whole rule, in one function. A record missing anything is not partial — it is undefined,
 *  and undefined fails closed. The point is not moral: "undefined" produces a work queue with a
 *  named missing field per item, where "too many identities" produces a meeting. */
export function classify(record: CredentialRecord, ctx: ClassifyContext): Definition {
  const deficiencies: Deficiency[] = [];

  if (record.principal.trim().length === 0) {
    deficiencies.push({ field: "principal", why: "no workload named, so nothing can be checked for existence" });
  }
  if (record.owner.resolutionExpiresAt <= ctx.now) {
    deficiencies.push({ field: "owner.resolutionExpiresAt", why: "owner not re-resolved in the directory within the freshness window" });
  }
  if (record.purpose.actionClassRef.trim().length === 0) {
    deficiencies.push({ field: "purpose.actionClassRef", why: "no action class referenced, so purpose is unfalsifiable" });
  }
  if (record.purpose.deliverableRef.trim().length === 0) {
    deficiencies.push({ field: "purpose.deliverableRef", why: "not traceable to a deliverable, so nothing retires it when the work ends" });
  }
  if (record.expiry.kind === "none") {
    deficiencies.push({ field: "expiry", why: "no expiry and no rotation, so nothing fails closed on its own" });
  }
  if (record.revocation.runbookRef.trim().length === 0) {
    deficiencies.push({ field: "revocation.runbookRef", why: "no named runbook, so no one is identified who can execute the removal" });
  }
  if (ctx.drill === null) {
    deficiencies.push({ field: "revocation.drill", why: "no drill has ever observed a refusal through this binding" });
  } else if (ctx.drill.result !== "refused") {
    deficiencies.push({ field: "revocation.drill", why: "the last drill timed out with the canary still being accepted" });
  } else if (ctx.drill.ranAt + ctx.drillFreshnessSeconds <= ctx.now) {
    deficiencies.push({ field: "revocation.drill", why: "the last successful drill is older than the freshness policy" });
  }

  return deficiencies.length === 0
    ? { status: "defined", record }
    : { status: "undefined", record, deficiencies };
}

What is deliberately absent: any discovery of credentials the engagement did not create, any policy decision about what a credential may do, and any storage adapter. The first is out of scope by design, the second belongs to the client, and the third is the one part that must be written against the client's own datastore — because a register living in the delivery firm's tooling leaves when the delivery firm does.

The control path, in an estate you do not own

Seven steps, and the interesting property of the sequence is how little of it is new construction. Five of the seven touch components that already exist on the engagement.

  1. An engineer needs a credential. They call the same module, pipeline job or runbook they were already calling — the hook is inside it, not beside it. This is the step that determines whether the design survives contact with a delivery schedule.
  2. The hook resolves the owner against the client's directory. A principal that does not resolve stops here, which is the correct behaviour and will be unpopular the first time it fires on a contractor whose account was provisioned late.
  3. The hook classifies the prospective record. Missing action class, missing deliverable reference, no expiry, no runbook, no drill on the binding: any one of these means undefined, and undefined refuses.
  4. The record is written to the client's register before the credential is minted. That ordering is deliberate and not interchangeable: a crash between the two leaves a record with no credential, which is noise you can clean up, rather than a credential with no record, which is the exact object this design exists to prevent.
  5. The credential is minted by whatever was minting it before. Nothing about the issuance path changes, which is what makes this insertable into an engagement already in flight.
  6. On a schedule, the drill runs per binding: canary minted, acceptance proven at a real resource server, revocation executed, polled to refusal, interval recorded. Bindings that time out are recorded as findings rather than swallowed.
  7. At each release, the manifest is built. Any undefined record blocks it; any record still owned on the firm's side with no named receiving client principal blocks it. Handover is the gate, and it runs at every release so that the last one is uneventful.
FIGURE 2 · REFERENCE ARCHITECTURE Six layers, inside an estate you do not own One new component. Everything below it is the client’s, unchanged, because no migration is available. The rails are the design: a row populated late, or owned by the firm, does not survive handover. THE LAYER POPULATED WHEN OWNED AFTER 6 Handover manifest & acceptance gate Signed transfer: every credential created, its owner at transfer, the named receiving owner, and each observed revocation interval. An acceptance condition, not a closing slide. Every release not closedown Client alone 5 Revocation drill Mint a canary, confirm a real resource accepts it, revoke, poll until a real refusal is observed, record the interval against the binding. This is what makes “tested” a fact rather than an adjective. From week one continuously Client job + runbook 4 The register — the only new component Append-only seven-field records, in the client’s own repository and datastore. Never in the firm’s tooling, not even temporarily. A register the firm hosts is a register that leaves when the firm does. At creation same commit Client from commit one 3 Creation hooks at the existing issuers A thin wrapper around the module, the pipeline job and the console runbook the engagement already uses. No new issuance path. Refuses to complete when the record would be undefined. At creation enforced there Joint → client their pipelines 2 The client’s existing identity systems Cloud IAM roles, cluster service accounts, the directory, the vault, the SaaS admin consoles. Whatever is already there. UNCHANGED — no migration is available inside someone else’s estate. Pre-existing Client already was 1 The inherited estate Untouched. It is not yours, and it was not in scope. n/a Client Nothing in the left column is a platform. Every row terminates in the right-hand rails. Layers 5 and 6 must keep working with the firm gone. If they cannot, the deliverable did not survive.

Two placement decisions carry most of the risk of the design being quietly hollowed out. The first is that the register lives in the client's repository and the client's datastore from the first commit — not migrated there at the end, not mirrored, not hosted by the firm with an export promise. An export promise is a promise, and promises made in month two are kept by people who have left by month fourteen. The moment the register is a table in the client's own store with a schema in the client's own repository, its survival stops depending on anybody's goodwill.

The second is that the drill has to run against real resource servers in an environment the client operates. A drill against a staging endpoint the firm stood up is measuring the firm's staging endpoint. It is also the step most likely to be argued down to a synthetic check on the grounds that production drills are risky, and that argument should be met on its merits rather than dismissed: the canary must be a genuinely throwaway principal with no access beyond a single innocuous read, and the drill has to be rate-limited and owned by an on-call rota. What cannot be conceded is the resource server. Move that, and the number stops meaning anything.

Revocation is a measurement, not a call

The drill's output is one number per binding, and it is worth being precise about what that number is and is not, because this is the area where confident-sounding claims outrun the evidence most badly.

What the number is: the interval, observed once under the conditions of that run, between executing the documented revocation and a real resource server refusing a credential it had previously accepted. What it is not: a guarantee, a service level, or a distribution. A single drill is one sample. Repeated drills give you a series, and a series is worth publishing; one drill is worth exactly the claim that the path is not fictional.

The vendors themselves publish bounds that make the shape of the problem clear, and they are worth putting side by side because the spread is the finding. AWS documents that revoking a role session does not invalidate the credential at all — the console action attaches an inline deny policy covering sessions assumed in the past and approximately thirty seconds into the future, a forward-dating it explains as accounting for the propagation delay of the policy so that a session acquired or renewed just before the update is still caught. The AWS command line interface user guide supplies the other half: the CLI caches credentials until they expire, and where a role’s temporary credentials have been revoked the operator has to delete the local cache by hand. Revocation there is a policy race with a manual cleanup step, not a credential death.

Google publishes explicit bounds and is unusually direct about the consequence. Its IAM documentation states that access changes are eventually consistent, that policy changes take typically two minutes and potentially seven minutes or longer, that group membership changes take typically several minutes and potentially hours or longer, and that in the meantime recent access changes might not be effective everywhere — principals might still be able to use a recently revoked role or a recently denied permission. Microsoft publishes the exposure window for its own platform from the other end: access tokens are assigned a random default lifetime between sixty and ninety minutes, seventy-five on average, with long-lived token lifetimes running from twenty to twenty-eight hours. And Kubernetes documents a class with no window at all, because Secret-mounted service account tokens do not expire and do not rotate.

FIGURE 3 · THE INTERVAL, AND HOW TO OBSERVE IT How long a revoked credential can still be honoured Bounds each vendor publishes about its own platform. Ordered by magnitude, not drawn to scale. seconds never AWS IAM ~30 s margin a deny policy dated forward to absorb propagation delay; CLI cache cleared by hand GOOGLE CLOUD IAM 2 – 7 min + policy change propagation; group membership: several minutes to hours or longer MICROSOFT IDENTITY 60 – 90 min default access-token life, 75 on average; long-lived lifetimes run 20 to 28 hours KUBERNETES SECRET TOKENS no expiry the project’s own docs call them not recommended: they do not expire and do not rotate Documented bounds on exposure, published by each vendor about its own platform. Not measurements of how often revocation fails — no primary publisher was found for that, and this figure does not supply one. THE DRILL — WHAT REPLACES THE BELIEF WITH A NUMBER 01 Mint a canary against the binding on the record, using the same issuer the real credential uses 02 Prove acceptance at a real resource server. A canary that was never accepted proves nothing 03 Revoke RFC 7009 returns 200 both for a successful revocation and for an invalid token 04 Poll to refusal at the same real resource server, until it genuinely stops accepting 05 Record it on the record, and in the handover manifest, once per binding The output is one observed number per revocation binding. It is the only evidence that the path works. A binding whose drill has never run is not fast or slow. It is undefined, and it fails closed.

One thing I want to be careful not to imply. These are documented bounds on exposure, published by each vendor about its own platform. They are not measurements of how often revocation silently fails to take effect at all, and I could not find a primary publisher for that. There is no study I can point to giving an observed distribution of time-to-actual-revocation across real estates, and the honest position is that the open question is open. Treating the vendor bounds as though they were failure rates would be laundering, and it is the specific error this section exists to avoid.

Where this design breaks

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

Credentials created outside the hooks. This is the big one, and no amount of typing fixes it. The hook covers the module, the pipeline and the runbook. It does not cover an engineer in a console at eleven at night during an incident, a vendor's own onboarding wizard that provisions an integration principal on your behalf, or a subcontractor working in a repository you never see. The interagency guidance's own concern about extended chains of subcontractors is the same worry from the client's side. The mitigation is a weekly reconciliation between what the client's issuers show as created-this-week and what the register holds — which is a discovery mechanism, which I said was out of scope, and which sneaks back in through the door marked coverage. I do not have a way around that. What I would defend is the ordering: a hook plus a narrow reconciliation over one week's creations is tractable, where a general discovery programme over a whole estate is the thing that never ships.

Ownership transfer that nobody actually accepted. The acceptance gate checks that a named client principal has been recorded as the receiving owner. It cannot check that the person knows, agrees, or has any capacity to act. The predictable degeneration is a single infrastructure manager appearing against nearly every line because they were the one in the handover meeting. The design makes this visible — a concentration query over the manifest surfaces it immediately — and visibility is a weaker property than prevention. It is also the strongest property available to a party who will not be there next quarter.

Purpose decays into one class. Nothing stops a client defining a single action class called “platform operations” and pointing everything at it. Once that happens the purpose field costs maintenance and buys nothing. The deliverable reference is more robust because it is checkable against a document that exists independently, which is precisely why it is a separate field rather than free text inside the purpose statement — but a determined team can put the same deliverable reference on everything too. Legibility, again, rather than enforcement.

The drill measures the binding you were willing to drill. Bindings differ enormously in how comfortable an organisation is drilling them. An OAuth revocation endpoint against a read-only test client is easy. A key-deletion runbook against a production vault mount is not, and the pressure will be to mark those bindings exempt. An exempt binding is an undefined binding wearing a policy label, and a register with a large exempt set has the same epistemic status as no register — with the added harm of a coverage number that looks reassuring. If exemptions exist, publish their count next to the coverage number or the coverage number is misleading.

Failing closed at creation is an outage mode wearing a security costume. The hook refuses to create a credential when the record would be undefined. During an incident, at two in the morning, that refusal is a delivery team blocked from fixing production because a directory lookup is slow or an action class has not been defined yet. There has to be a break-glass path, and the moment there is a break-glass path there is a thing that gets used routinely by week three. The least bad arrangement I know is a break-glass that succeeds, records the record as undefined with an explicit exemption marker and an expiry on that marker, and pages somebody — so the escape hatch produces a work item instead of a silence. I would not claim that solves it.

Records outlive the delivery firm's ability to correct them. Every field in this record is written by people who leave. Owner re-resolution bounds one kind of decay. Nothing bounds the decay of a purpose statement written by a consultant in month three that stopped being true in month nine, and there is no mechanical test for “this sentence is now false”. The design is honest about this only insofar as the deliverable reference gives a proxy: when a deliverable is decommissioned, everything pointing at it is suspect. That is a coarse instrument and it is the best one here.

It says nothing about what the credential can do. This register knows who owns a credential, why it exists, and how to take it away. It makes no statement whatsoever about whether the authority attached to it is appropriate, or about what three individually reasonable grants compose into. That is a genuinely harder problem, it lives in the client's policy plane, and a firm selling this design as an answer to it is selling something else. Knowing you can revoke a credential is not the same as knowing you should not have granted it.

And it covers only what you created. The estate you inherited is untouched by all of this. If the client has four hundred pre-existing service identities of unknown provenance, this design leaves four hundred pre-existing service identities of unknown provenance and adds a clean set alongside them. That is a real limitation and it is also the reason the design is deliverable at all: the scope that makes it honest is the scope that makes it finishable.

What it costs

Some of this is an engineering fact about the design. Some of it is a measurement 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.

Creation-path latency. The hook adds a directory resolution, a drill-freshness lookup and a durable write before the mint. The first two are cacheable — resolve the owner on a schedule, not on every creation — leaving the durable write as the only unavoidable addition, and its cost is a property of the client's datastore rather than of this design. The prediction under test is that the added latency is irrelevant in practice because credential creation is not a hot path: it happens at deploy time and at provisioning time, measured in tens per week, not thousands per second. I have not instrumented this on a real engagement and will not invent a figure.

The drill's operational cost is the real one. It is a scheduled job that mints, presents, revokes and polls against production resource servers, and it needs an owner, an alerting path and a rate limit. Its cost scales with the number of distinct bindings, not with the number of credentials, which is why the per-binding decision earlier matters so much: on a typical integration engagement I would expect a handful of bindings rather than dozens. That is an expectation from the shape of the design, not a measurement, and the first engagement to run it should publish the actual count.

Build effort. The record type and the classifier are an afternoon. The creation hook is a few days per issuer, and the number of issuers is the variable that actually determines the size of the job — a client with cloud IAM, a cluster and a vault is three adapters, and a client with eleven SaaS admin consoles is a different conversation. The canary harness is the fiddly part, because each issuer needs its own mint, present and revoke, and “present to a real resource server” means finding a genuinely harmless read on each one. The manifest and the gate are a day. What is not a day is the negotiation about who accepts ownership, and that should start in week one rather than at closedown.

The contractual cost, which is where the resistance will actually come from. Making the manifest an acceptance condition means the engagement cannot close cleanly while credentials remain unowned — which transfers a risk that currently sits, unpriced and invisible, with the client, onto the firm's schedule. That is a real commercial cost and it should be priced rather than absorbed quietly. It is also a position in a competitive procurement, because a bid that has never counted the identities it will create cannot answer the question once somebody has asked it in the room, and questions of that kind are difficult to un-ask.

If you only had a week

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

  1. Write the record type and make it total, with no optional fields. An afternoon, and it forces the argument about what completeness means to happen once in a pull request rather than repeatedly in status meetings.
  2. Instrument the creation paths to emit what they would have recorded, and gate nothing. Within two days you have a census of this week's creations and which field would have been missing in each — a number the account does not currently have and which costs nothing politically to produce.
  3. Build the canary harness for exactly one issuer, and run the drill once. The output is one observed revocation interval for one binding, and it will be the first time anyone on the engagement can state that number rather than assume it.
  4. Turn the gate on for one credential class, ideally one your own team owns, so the first blocked deployment is yours and the objections get heard internally before a client engineer meets them.
  5. Produce the manifest for what exists so far and take it to the next steering meeting — not as a governance artefact, but as one page listing what the engagement has created and who currently owns each item. The conversation that page starts is the deliverable.

What you have at the end of that week is not a lifecycle programme. It is two numbers — how much of this week's credential creation could not have produced a complete record, and how long one revocation path actually takes — plus a page that names owners. Nothing in the instruments above asks an engagement to produce any of the three. All three are cheap, and all three change what the client asks for next.

The reason to build it at creation rather than at closedown is worth restating plainly, because it is the only genuinely structural advantage in the problem. At the moment a credential is created, one person knows everything the record needs and it costs them a line to write it down. At closedown, nobody knows, the person who did has moved on, and the same record costs an investigation that will produce guesses. Everything in this piece is downstream of that asymmetry. The instruments will not force it — they name data, information and resources at termination, and a resignation as the removal trigger — so it will be forced by a statement of work or it will not be forced at all. Which is to say: by the firm that decides to write it in.