Case study
Okta access review in AWS
A quarterly Okta access review that runs in AWS. The CISO decides every item in Slack, each fix becomes a Jira ticket, and the evidence is kept for audits.
What this shows
- IAM judgement. 18 checks, including the leaver cases an account status doesn't show, like a service account the leaver owned that keeps running after they go.
- Evidence an auditor can check: a SHA-256 manifest, a sign-off bound to that hash, and create-only storage under S3 Object Lock.
- All of it runs in AWS as Terraform, deployed from GitHub Actions through OIDC, so there are no stored AWS keys.
The problem
A quarterly access review tends to be a spreadsheet. Someone marks each row keep or revoke, the revokes may or may not happen, and nobody goes back to check.
Offboarding has a gap of its own. Deactivating someone’s Okta account doesn’t touch the service accounts they own, or GitHub access that wasn’t set up through Okta. Those keep working, and a normal review never looks at them. This one does.
Be the CISO
Be the CISO
You're the CISO at Acme, a fictional company, and the quarterly access review just opened. Decide every item, sign off, and see the tickets it opens. Then change one byte of the evidence and watch the check catch it.
Nothing here is mocked up. The review, the messages and the tickets are the output of the tool's own code at one commit, and this page replays them. A test checks the replay matches the tool byte for byte.
The demo runs here once its script loads. Without JavaScript, the screenshots below show the same review.
The same review as screenshots
These are from a demo run with Acme sent through the same code the Lambda functions run. The reviewer’s name is blacked out.




Every run also writes a PDF report. The sample report is from the same fictional company, and it’s incomplete on purpose: the demo plants four data gaps, and the report leads with them.
How it runs in AWS
An EventBridge schedule starts a Step Functions workflow each quarter. Eight Lambda functions share one arm64 container image: they read Okta, post to Slack, handle the button clicks, open the tickets, send reminders, and re-read Okta every day to check the fixes.
All of it is Terraform, deployed from GitHub Actions through OIDC after an approval. The only step done by hand is storing four secret values, which Terraform deliberately never holds.
Why the evidence can be trusted
- It can’t change Okta. Okta grants only read scopes, the tool refuses any other scope, and its client can only send GET requests. A test enforces the last one.
- Every file is hashed.
manifest.jsonlists the SHA-256 of every file in a run, and the CISO’s sign-off is bound to that hash. - Nobody can quietly edit a past review. The evidence goes to S3 create-only, under Object Lock.
- A failed read is never reported as clean. If a source can’t be read in full, the report is marked incomplete and names the source.
- Fixes are checked. When a ticket is resolved, a daily job reads Okta again and flags the ticket if Okta still shows the problem.
- A stolen token is useless. Private Key JWT, and DPoP binds each access token to a key that exists only in memory for that run.
SOC 2 and ISO 27001
Each of the 18 checks maps to controls in both frameworks. The full list is in the README.
| What it checks | SOC 2 | ISO 27001:2022 |
|---|---|---|
| Leavers whose account, API token or sign-ins outlast their last day | CC6.2, CC6.3, CC7.2 | A.5.18, A.8.16 |
| Accounts with no HR record, contract end dates, inactive and unused accounts | CC6.2 | A.5.16, A.5.18 |
| Missing MFA | CC6.1 | A.8.5 |
| Admin users, and service apps that can make changes | CC6.3 | A.8.2 |
| Service accounts and credentials a leaver owned or held | CC6.1, CC6.2, CC6.3 | A.5.16, A.5.17, A.5.18, A.8.2 |
| The tool’s own changes: pull requests, required checks, signed evidence | CC8.1 | A.8.29, A.8.32 |
Trade-offs
- Only Super Administrator can read admin role assignments in Okta. The app has it, paired with read-only scopes, and flags itself for review on every run.
- The HR roster is a CSV export passed in at run time. There’s no live HR integration yet.
- GitHub is read from a snapshot file. The AWS pipeline doesn’t pass one yet, so its runs are Okta only.
- MFA status comes from enrolled factors. It doesn’t check whether a sign-on policy requires MFA.
- Some fixes are taken on the reviewer’s word, because Okta can’t show them. A rotated client secret is one.
Run it yourself
The demo needs no Okta, AWS, Slack or Jira. With uv installed, clone the repository and run:
uv run access-review \
--snapshot fixtures/demo_snapshot.json \
--roster fixtures/demo_roster.csv \
--config fixtures/demo_config.json \
--github fixtures/demo_github.json \
--as-of 2026-09-15
That reviews Acme, which has at least one planted case for each check, and writes the report to
reports/. To run the whole AWS workflow in memory, Slack and Jira included, run
uv run python scripts/e2e_local.py.