The short version
- SSO deactivation only covers apps that authenticate through the identity provider. IAM users, access keys, deploy keys, bot tokens and shared secrets don't.
- Even inside SSO, sessions outlive deactivation: AWS IAM Identity Center role sessions continue until they expire, up to 12 hours.
- Find what the person holds (keys on their IAM user, tokens on their GitHub account) and what they created (IAM users, keys and webhooks other systems now depend on).
- Before removing anything, check whether it's still used and by what. Personal credentials go now. Credentials a system depends on are moved to a role first, then deactivated, watched and deleted.
- Measure "access gaps": accounts and credentials that don't match what the person's current role should have. Offboarding is finished when that number is zero, not when the ticket is closed.
1. Why SSO deactivation isn't enough
Most offboarding runbooks start and end with the identity provider. That's the right first step, and for a sales or finance hire it may be most of the job. For an engineer it isn't, because engineers create credentials that never pass through SSO. Here is what survives, with the vendor documentation behind each point.
| Credential | What happens when the IdP account is disabled |
|---|---|
| AWS IAM users and access keys | Nothing. They live in each AWS account, independent of your IdP. A key keeps working until someone deactivates or deletes it. |
| AWS IAM Identity Center role sessions | New sessions are blocked, but existing IAM role sessions continue for the permission set's session duration, which can be up to 12 hours (AWS). |
| GitHub organisation membership | With SAML SSO but without SCIM, members removed from the IdP "aren't automatically removed from the organization", and authorised tokens keep granting access after their SSO sessions expire (GitHub). |
| GitHub deploy keys | Attached to a repository, not to the person. The API records who added each one (added_by) and when it was last used (last_used). |
| Slack apps they installed | Their API tokens are revoked, but bot users, slash commands and incoming webhooks "will remain active" (Slack). |
| Okta app sessions | Okta's revoke-sessions call ends Okta sessions and can revoke Okta-issued OAuth tokens, but Okta notes it "doesn't clear the sessions created for web or native apps" (Okta). An app that keeps its own session keeps the user signed in until that session ends. |
| Shared secrets they could read | Unaffected. Database passwords, third-party API keys and break-glass credentials in a vault stay valid until rotated. |
It helps to split the problem in two. Credentials the person holds (keys on their own IAM user, tokens on their GitHub account) should simply go. Credentials the person created for something else (a "deploy" IAM user, a deploy key, a webhook, a service account) are the dangerous ones: they're often still in use, and removing them blindly is how offboarding causes outages. The rest of this guide treats them differently.
2. Find a leaver's AWS credentials
Run these in every AWS account you have, not just the main one. With AWS Organizations, loop over the member accounts with a read-only role.
Every IAM user and key, with last use
The IAM credential report is a CSV of every IAM user with password_enabled, access_key_1_active, access_key_1_last_used_date, access_key_1_last_used_service and the same for key 2. You can regenerate it at most once every four hours.
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 --decode > creds.csv
Two caveats from AWS's own documentation: the report only covers the first two access keys per user, and it doesn't include service-specific credentials such as CodeCommit Git credentials or Amazon Bedrock API keys. For a complete answer, list per user:
for u in $(aws iam list-users --query 'Users[].UserName' --output text); do
for k in $(aws iam list-access-keys --user-name "$u" \
--query 'AccessKeyMetadata[].AccessKeyId' --output text); do
printf '%s\t%s\t' "$u" "$k"
aws iam get-access-key-last-used --access-key-id "$k" \
--query '[AccessKeyLastUsed.LastUsedDate,AccessKeyLastUsed.ServiceName,AccessKeyLastUsed.Region]' \
--output text
done
aws iam list-service-specific-credentials --user-name "$u" \
--query 'ServiceSpecificCredentials[].[ServiceName,Status]' --output text
aws iam list-ssh-public-keys --user-name "$u" \
--query 'SSHPublicKeys[].[SSHPublicKeyId,Status]' --output text
done
get-access-key-last-used returns the date, service and region of the most recent request. A key that has never been used returns no date and N/A for service and region.
What the person created
An IAM user called ci-deploy won't have the leaver's name on it. To find users and keys they created, search CloudTrail for the IAM calls made by their identity. IAM is a global service, so its events are recorded in us-east-1:
aws cloudtrail lookup-events --region us-east-1 \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreateAccessKey \
--output json --query 'Events[].CloudTrailEvent' \
| jq -r '.[] | fromjson
| [.eventTime, .userIdentity.arn, .requestParameters.userName,
.responseElements.accessKey.accessKeyId] | @tsv'
Repeat with CreateUser, CreateLoginProfile and CreateRole, then filter on the leaver's identity in userIdentity.arn. If they worked through IAM Identity Center, the ARN looks like assumed-role/AWSReservedSSO_…/<session name>; check a known event of your own first to confirm what your session names contain. Event history only covers the last 90 days of management events, one region at a time, and allows two lookups per second. For anything older, query your trail's logs in S3 (with Athena) or CloudTrail Lake. That's a good reason to tag IAM users with an owner when they're created, so next time the answer is one query.
Who is still using a key
"Last used 2 hours ago" tells you a key matters. It doesn't tell you what uses it. Look up the key's recent calls in each region where it's used:
aws cloudtrail lookup-events --region eu-west-1 \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIA... \
--output json --query 'Events[].CloudTrailEvent' \
| jq -r '.[] | fromjson | [.eventTime, .eventName, .sourceIPAddress, .userAgent] | @tsv' \
| sort | uniq -c | sort -rn | head
A stable private IP with an SDK or Jenkins user agent is a machine. A changing public IP with a browser or CLI user agent is a person. Event history only holds management events. If the key is mostly used for S3 object reads and writes, you'll only see those calls if your trail logs data events.
3. Find GitHub tokens, SSH keys and deploy keys
The single most effective GitHub step is removing the person from the organisation. GitHub says that after removal their personal access tokens, SSH keys and app authorisations "can no longer be used to access your enterprise's and organizations' resources" (GitHub). Outside collaborators are the exception: they're removed repository by repository. Before you remove the member, list what they authorised, because anything that breaks afterwards will trace back to it.
SAML SSO credential authorisations (GitHub Enterprise Cloud with SAML SSO; organisation owner). This lists the person's tokens and SSH keys authorised for the org, with when each was last used:
gh api --paginate "/orgs/ORG/credential-authorizations?login=USERNAME" \
--jq '.[] | [.credential_id, .credential_type, .token_last_eight,
.credential_accessed_at, .authorized_credential_title] | @tsv'
# revoke one authorisation (removes the SAML authorisation, not the token itself)
gh api -X DELETE "/orgs/ORG/credential-authorizations/CREDENTIAL_ID"
Fine-grained personal access tokens with access to the org. These endpoints only accept GitHub App authentication, so call them with an app installation token, not your own:
curl -s -H "Authorization: Bearer $INSTALLATION_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/orgs/ORG/personal-access-tokens?owner[]=USERNAME"
# fields include token_name, token_last_used_at, token_expires_at
curl -s -X POST -H "Authorization: Bearer $INSTALLATION_TOKEN" \
"https://api.github.com/orgs/ORG/personal-access-tokens/PAT_ID" \
-d '{"action":"revoke"}'
Deploy keys belong to repositories, so they stay after the person leaves. Check each repository for keys they added:
for r in $(gh repo list ORG --limit 1000 --json name --jq '.[].name'); do
gh api "/repos/ORG/$r/keys" \
--jq ".[] | select(.added_by==\"USERNAME\") | [\"$r\", .title, .last_used, .read_only] | @tsv"
done
Then remove the member with gh api -X DELETE /orgs/ORG/members/USERNAME. Removed members lose access to private forks of your private repositories, though GitHub notes they may still have local copies. Also check your Actions secrets and CI variables for tokens named after the person. A token they generated for a pipeline stops working the moment they're removed, so it's better to know about it now than at the next release. If you use Enterprise Managed Users, accounts are suspended from the IdP instead of removed.
4. Google Workspace, Okta and Slack
- Google Workspace. List third-party apps the user granted OAuth access to with
GET /admin/directory/v1/users/{userKey}/tokens, and remove each withDELETE …/tokens/{clientId}(Directory API).POST …/users/{userKey}/signOutsigns the user out of all web and device sessions. Transfer Drive ownership before deleting the account. - Okta.
DELETE /api/v1/users/{userId}/sessions?oauthTokens=truerevokes Okta sessions and the OAuth and OIDC tokens Okta issued to the user. Remember the caveat above about apps with their own sessions. Okta warns that deactivating a user is destructive: it deprovisions them from every assigned app, which can destroy data such as email or files. Transfer ownership first, or suspend while you do. - Slack. Deactivation revokes the member's API tokens, but bot users, slash commands and incoming webhooks they set up keep running. Review apps and custom integrations installed by the person, and reassign the ones you want to keep.
5. Remove without breaking things
For each credential on your list, answer three questions: who holds it, when was it last used, and what uses it. Then follow the matching path.
- Personal and unused, or used only by the person: remove it at the leaving time. For an IAM user, AWS's CLI procedure deletes the login profile, access keys, signing certificates, SSH keys, service-specific credentials and MFA devices, removes inline and managed policies and group memberships, and then deletes the user.
- Used by a system: don't delete it on the leaving day. Move the system to a role first: an instance or task role, or OIDC federation from your CI provider. Then deactivate the key, which is reversible:
Watch the dependent job's own logs for authentication errors for a period that covers its schedule (a weekly job needs more than a day). If something breaks, set it back toaws iam update-access-key --user-name ci-deploy \ --access-key-id AKIA... --status InactiveActive. Once the window passes cleanly, delete the key. - Can't be moved yet: keep it, but give it an owner, a due date and an alert on use from anywhere other than the known source. Rotate it, because the leaver may have seen the secret.
AWS's own guidance points the same way. Its key update procedure says to deactivate an old key rather than delete it immediately, even when it looks unused, and its user removal page says to review recent activity before removing a principal. To block an IAM user without deleting anything, AWS suggests attaching an explicit deny-all policy.
Live AWS sessions. If the departure is hostile or credentials may be compromised, the 12-hour Identity Center window matters. AWS's revocation procedure adds the user's ID to a prepared deny policy in an SCP, removes their permission set assignments, disables them in the IdP, deletes their active sessions in IAM Identity Center and only then disables user access. The deny stays in place for at least 12 hours. Order matters: AWS warns that if you disable access before ending the session, you can't end it from the console without re-enabling the user first.
After deletion, you're blind. Once a key is deleted, a later attempt to use it simply fails. If you want a reliable alarm on anyone using credentials they shouldn't have, plant a deliberate decoy rather than keeping a leaver's real key around. Our AWS canary credentials guide shows how to build one with a deny-all IAM user and CloudTrail alerts.
6. Engineering offboarding checklist
Before the last day
- Inventory: IAM users, keys and service-specific credentials in every AWS account; GitHub credential authorisations, fine-grained PATs and deploy keys; Google OAuth grants; Slack apps and webhooks; vault items they could read.
- Search CloudTrail (and older logs) for IAM users, keys and roles they created.
- For each credential, record last use and what uses it. Move system dependencies to roles.
- Transfer ownership: documents, shared drives, repositories, on-call schedules, scheduled jobs.
At the leaving time
- Disable in the IdP and revoke sessions and tokens (Okta, Google, Microsoft 365).
- End IAM Identity Center sessions; use the deny-policy procedure if the departure is high-risk.
- Remove from the GitHub organisation; remove outside-collaborator access repo by repo.
- Delete personal IAM users and keys. Deactivate, don't delete, anything a system uses.
- Rotate shared secrets they could read.
After
- Verify each step by reading the target system back, not by trusting the API response.
- Delete deactivated keys once the watch window passes. Close open items with an owner and a date.
- Keep the record: what was found, what was removed, who approved what, and when.
7. Measuring access gaps
"Offboarding ticket closed" measures the process, not the outcome. A better number is access gaps: every account or credential that doesn't match what a person's current role should have. A leaver should have zero access, so everything they still hold is a gap. A new starter missing something their role needs is also a gap, as is a mover still holding their old team's access.
To compute it you need two lists. Expected comes from your HR system plus role templates: who works here, in which role, and what that role should have. Actual comes from reading each system: IdP users and groups, IAM users and keys, GitHub members and credentials, app accounts. The gap is the difference, matched by person. Credentials with no person behind them, such as an IAM user whose creator left, count too, until someone owns them.
Track three things: the total each day, how long each leaver's gaps take to reach zero, and how many open gaps have a named owner and due date. A gap you know about, with an owner, like a key waiting on a pipeline change, is fine. A gap nobody knows about is the one that hurts. Whatever you build should keep reading this number back after it acts, rather than assuming an API call worked.
8. Tools that help, and where they stop
You don't have to script all of this. Joiner-mover-leaver tooling is mature for anything connected to your IdP:
- Okta Workflows: no-code lifecycle flows with templates and a Connector Builder for apps without a ready connector. Check with Okta which edition includes it.
- Microsoft Entra lifecycle workflows: joiner, mover and leaver workflows on Entra users, extendable with Logic Apps. Requires Microsoft Entra ID Governance or Microsoft Entra Suite licences.
- Lumos, ConductorOne (now branded C1) and BetterCloud: HR-triggered provisioning and deprovisioning across SaaS, access requests and reviews. C1 also positions itself on non-human identities and AI agents, and BetterCloud on SaaS management and shadow IT.
Use them. Where they tend to stop is the subject of this guide: credentials a person created inside AWS or GitHub for a system, checking usage before removal, and verifying each removal by reading it back. Each vendor's coverage varies, so ask them directly about IAM users created outside SSO, deploy keys and webhooks, and test with a real leaver.
If you want that last stretch handled by one system, see this design working in the Quartermaster demo: a mock run where a leaver's legacy IAM key turns out to be used by a build server, so it's moved to a role before it's removed. We build systems like this for specific environments as part of Custom Autonomous Systems Engineering.
9. Limits and what we couldn't verify
- Failed calls with a deactivated key. We haven't confirmed which services record a call made with an inactive key in CloudTrail. Watch the dependent job's own logs rather than relying on CloudTrail for this.
- Plan-dependent features. GitHub's credential authorisation API needs SAML SSO, which is a GitHub Enterprise Cloud feature. Entra lifecycle workflows need Entra ID Governance or Entra Suite. Check Okta Workflows availability for your Okta edition.
- Vendor coverage changes. The tool descriptions above come from each vendor's own site on 10 October 2026. Confirm specific connectors with the vendor.
10. FAQ
Does deactivating someone in Okta or Entra ID remove their AWS access keys?
No. IAM users and their access keys live in each AWS account and aren't linked to your identity provider, so they keep working until someone deactivates or deletes them. Disabling the person does block new IAM Identity Center sessions, but existing role sessions continue until they expire, which can be up to 12 hours.
How do I find AWS access keys a former employee created?
List every IAM user's keys with the credential report or list-access-keys, then search CloudTrail for CreateUser and CreateAccessKey events made by that person's identity. IAM events are recorded in us-east-1. CloudTrail event history only covers the last 90 days of management events, so older history needs your trail's logs in S3 or CloudTrail Lake.
Should I delete or deactivate a leaver's access key?
Delete keys only the person used. If a system uses the key, move that system to a role first, then deactivate the key, which is reversible, and watch the job's logs for a period that covers its schedule. Delete it once nothing has broken. AWS itself recommends deactivating before deleting, even for keys that look unused.
Do GitHub personal access tokens stop working when someone leaves?
Not on their own. Once you remove the person from the GitHub organization, GitHub says their personal access tokens, SSH keys and app authorizations can no longer access the organization's resources. With SAML SSO but without SCIM, removing them from the identity provider doesn't remove them from the organization, so you must remove them in GitHub too. Deploy keys belong to repositories and stay until you delete them.
What is an orphaned account?
An account or credential whose owner has left or was never recorded, such as an IAM user created for a build server by an engineer who has since gone. It usually still works, and nobody is watching it. Give each one an owner or remove it after checking that nothing depends on it.
How do I know offboarding is actually finished?
Compare what each person should have, from your HR system and role templates, with what each system actually shows, and count the differences: access gaps. A leaver should have zero. Read each system back after every change instead of trusting the API response, and track any remaining gap with an owner and a due date.
Written by Ilia Dubovskii, founder of Beamreach. AWS, GitHub, Google, Okta and Slack behaviours were checked against their documentation on 10 October 2026. Vendors change things, so if something here no longer matches the docs, the docs win. Tell us and we'll fix this page.
Related: Quartermaster demo · AWS canary credentials · Custom Autonomous Systems Engineering · All guides for cloud teams
Beamreach