# The state of every credential in the account as of a stated moment, the enabled-or-disabled status of every workforce identity in the identity store, and the CloudTrail record of the revocations themselves — the five API calls that actually revoke standing access, each with the time it happened and the administrator who did it

> FedRAMP Consolidated Rules for 2026 v2026.07.14.01 · updated 2026-07-14
> Canonical page: /collect/personnel-separation-access-revocation

Recipe id: `personnel-separation-access-revocation` · cadence continuous · partial

> **Authored opinion.** AWS overlay v3.0.0, written
> against dataset 2026.07.14.01. The upstream
> FedRAMP rules name none of these tools; this mapping is ours.

## What it proves

- KSI `KSI-IAM-ELP`
- control `ps-4`

## Collection

Kind: `cli`

```sh
# generate-credential-report
aws iam generate-credential-report
# get-credential-report
aws iam get-credential-report
# list-users
aws identitystore list-users --identity-store-id <IDENTITY_STORE_ID>
# lookup-events-delete-user
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteUser --start-time <T0> --end-time <T1>
# lookup-events-delete-login-profile
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteLoginProfile --start-time <T0> --end-time <T1>
# lookup-events-delete-access-key
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteAccessKey --start-time <T0> --end-time <T1>
# lookup-events-update-access-key
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=UpdateAccessKey --start-time <T0> --end-time <T1>
# lookup-events-deactivate-mfadevice
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeactivateMFADevice --start-time <T0> --end-time <T1>
```

## Expected output

From generate-credential-report, a State of STARTED, INPROGRESS or COMPLETE — AWS stores ONE report per account and regenerates it at most once every four hours, so a request made inside that window silently returns the existing report rather than a fresh one. From get-credential-report, Content (the report, base64-encoded CSV), ReportFormat, and GeneratedTime — the timestamp that says what moment the evidence describes, and the field the freshness assertion reads. The CSV columns are: user, arn, user_creation_time, password_enabled, password_last_used, password_last_changed, password_next_rotation, mfa_active, access_key_1_active, access_key_1_last_rotated, access_key_1_last_used_date, access_key_1_last_used_region, access_key_1_last_used_service, the same five for access_key_2, cert_1_active, cert_1_last_rotated, cert_2_active, cert_2_last_rotated, and additional_credentials_info. Note what the report does NOT cover, because it bounds the whole recipe: AWS documents it as including only passwords, THE FIRST TWO access keys per user, MFA devices and X.509 signing certificates — service-specific credentials such as CodeCommit passwords are absent, as is any access key beyond the second, and additional_credentials_info is the only hint that more exist. From identitystore list-users, Users[] with UserId, UserName, ExternalIds and UserStatus, which is ENABLED or DISABLED. There are five lookup calls rather than one because lookup-events accepts exactly ONE lookup attribute per request, and revocation is five different API calls: deleting the user, deleting the console login profile, deleting an access key, setting an access key Inactive, and deactivating the MFA device. From each lookup-events call, Events[] with EventName, EventTime, Username — the REQUESTER who performed the revocation, not the identity revoked — and the CloudTrailEvent JSON, where requestParameters names the user or device that was acted on.

## Assertions

- {"field":"get-credential-report.GeneratedTime","op":"max_age_days","value":1,"controls":["ps-4"],"description":"The credential report being read describes a moment within the last day. AWS regenerates at most once every four hours and returns the stored report inside that window, so calling generate before get does NOT make the result fresh — GeneratedTime is the only field that says how old the evidence is. Substitute the interval your SSP commits to for revocation review."}

## GovCloud

IAM, the credential report and CloudTrail all operate in both AWS GovCloud (US) Regions and every call here is available; ARNs use partition arn:aws-us-gov. IAM Identity Center is available in both Regions but has NO multi-Region support, so list-users must be run against the Region its instance was enabled in. The CloudTrail calls carry the one real trap: IAM is a global service and its events are recorded in AWS GovCloud (US-West), us-gov-west-1, while Event history and lookup-events show these events in the Region where they occurred — so a DeleteUser lookup issued against us-gov-east-1 returns an empty Events[] and exits zero, which reads exactly like a period in which nobody was separated. The same shape applies in the commercial partition, where those events land in us-east-1.

## Notes

PS-04 is a list of things that happen when someone leaves: disable system access within a defined period, revoke the authenticators, conduct an exit interview, retrieve organizational property, retain access to the information the person worked on, and notify named personnel within a period. AWS holds evidence for the first two and nothing whatever for the other four.

And it holds those two only as state and history, never as subject. The credential report says every credential in the account and what shape it is in; CloudTrail says which revocation happened, when, and by which administrator. Five lookups rather than two, because lookup-events takes one lookup attribute per call and revocation is not one API: an estate that follows this recipe's own advice and DISABLES rather than deletes never emits DeleteUser at all, so a collection that watched only for deletions would return an empty Events[] on a correctly executed separation. UpdateAccessKey is in the list for the same reason — setting a key Inactive is a revocation that deletes nothing. What no call knows is that a PERSON was terminated. There is no roster in AWS, no employment status, no separation date — so the join between 'this human left on the 4th' and 'this identity was revoked on the 6th' is made by a human against an HR record, and the period the control defines is checked on that join and not here. This recipe produces the AWS-side column. It is partial for that reason and no assertion below pretends otherwise.

That is also why there is exactly one assertion. Any clause of the form 'no user has an active credential' is false on every working account and would be answering about the whole population when the control is about a named few; a clause over 'users your tagging marks as separated' would invent a third tagging convention to answer a question the HR record already answers better. So the single assertion is about the EVIDENCE rather than the estate: that the report describes a recent moment. A credential report is a snapshot with a timestamp, and a stale one is a true statement about a past that has already been superseded.

Read the four-hour rule as a collection constraint, not a detail. AWS stores one credential report per account and regenerates it at most once every four hours; a generate call inside that window returns the report you already had. So generate-then-get does not guarantee freshness, GeneratedTime is what does, and a collector that runs the two calls back to back and assumes the second reflects the first is reporting on a state up to four hours old.

The report's coverage is narrower than its name suggests, and the gap is exactly where a separation goes wrong. AWS documents it as covering passwords, the first two access keys per user, MFA devices and X.509 signing certificates. A third access key is not in it. Service-specific credentials are not in it. A separated user whose remaining access is a CodeCommit password or a long-term service credential appears in this report as fully revoked. additional_credentials_info is the only signal that anything else exists, and closing that gap needs ListAccessKeys and ListServiceSpecificCredentials per user, which this recipe does not collect and an assessment should.

On a well-run estate the IAM half is the small half. Workforce identities live in the identity store and a separation there is UserStatus DISABLED rather than a deletion, which is why the third call is collected: an account whose credential report lists no IAM user for a departed employee, while the identity store still lists them as ENABLED, has not revoked anything — and the credential report, which sees only IAM, cannot show it. Disabling rather than deleting is also the better practice for this control, because it preserves the identifier — see the IA-04 recipe on why a freed name is a hazard.

One KSI was deliberately not claimed. KSI-IAM-SUS asks that privileged accounts be disabled or secured IN RESPONSE TO SUSPICIOUS ACTIVITY, and a routine separation is not suspicious activity; the output is identical in both cases and cannot tell an assessor which one it is looking at. KSI-IAM-JIT was dropped for a quieter reason. Its statement asks for a least-privileged, role and attribute-based, and JUST-IN-TIME authorization model, persistently reviewed. Nothing in five commands over STANDING credentials evaluates a just-in-time or attribute-based model; only the 'persistently reviewed' tail is touched, and that tail is already what KSI-IAM-ELP carries here. KSI-IAM-ELP is earned outright: an identity that outlives its holder is precisely a failure of the rule that each user can access only what they need.

## References

- {"title":"IAM User Guide: generate credential reports — the CSV columns, the four-hour regeneration rule, and the documented exclusion of service-specific credentials and any access key beyond the first two","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html"}
- {"title":"IAM API: GetCredentialReport — returns Content (the base64-encoded CSV), ReportFormat and GeneratedTime, the timestamp the freshness assertion reads","url":"https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetCredentialReport.html"}
- {"title":"AWS CLI: identitystore list-users (Users[] with UserId, UserName, ExternalIds and UserStatus ENABLED | DISABLED; requires --identity-store-id)","url":"https://docs.aws.amazon.com/cli/latest/reference/identitystore/list-users.html"}
- {"title":"AWS CLI: cloudtrail lookup-events — events within the last 90 days; Username is the user or role name of the REQUESTER that called the API","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html"}
- {"title":"AWS GovCloud (US) User Guide: AWS CloudTrail — CloudFront, IAM and AWS STS events are recorded in us-gov-west-1, and lookup-events shows them in the Region where they occurred","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-ct.html"}
- {"title":"AWS GovCloud (US) User Guide: IAM Identity Center — \"Multi-Region support is presently not available\"","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-sso.html"}
