IAM Audit โ 2026 Q2
Snapshot of every IAM principal in the account on 2026-05-27, what it actually does, and what was tightened or deleted as a result.
This is a point-in-time audit, not a living document. The next audit produces a new file (iam-audit-2026q3.md) โ keeping each pass intact gives a diffable history of how IAM scopes evolved.
Inventoryโ
Users (3)โ
| User | Used for | Has password | MFA | Access keys | Verdict |
|---|---|---|---|---|---|
cli-user | Local AWS CLI work only (no console) | No | No | Yes | โ
correct โ member of sanctum-admins |
john-admin | AWS console sign-in (the human-facing admin, replaces root for day-to-day console work) | Yes | Yes | No | โ
added 2026-05-27 โ member of sanctum-admins |
toast-guilder-api-dev | Local docker-compose talking to dev buckets | No | No | Yes | โ
correct โ member of toast-guilder-app-dev |
Console sign-inโ
Account alias toast-guilder claimed 2026-05-27. Sign-in URL:
https://toast-guilder.signin.aws.amazon.com/console
Use john-admin for everything except billing, support plan, or account closure. Each root sign-in re-surfaces Policy:IAMUser/RootCredentialUsage in GuardDuty.
Groups (2)โ
| Group | Members | Permissions | Verdict |
|---|---|---|---|
sanctum-admins | cli-user | AdministratorAccess (managed) | โ correct โ break-glass + bootstrap, single member |
toast-guilder-app-dev | toast-guilder-api-dev | Inline app-dev-storage-rekognition โ scoped to dev buckets with explicit Deny * on prod buckets | โ correct โ tight, dev-only |
Roles in active use (all CDK-managed)โ
| Role | Trusted by | Permissions summary | Verdict |
|---|---|---|---|
SanctumServices-TaskExecRole-โฆ | ecs-tasks | Managed AmazonECSTaskExecutionRolePolicy + inline (secrets-by-ARN, ECR pull-by-repo, logs-by-group) | โ tight |
SanctumServices-ApiTaskRole-โฆ | ecs-tasks | Inline: S3 (now bucket-scoped, see "Tightenings"), Rekognition moderation, CloudWatch Insights queries, SSM Messages for ECS Exec | โ tight after audit fix |
SanctumServices-GdlTaskRole-โฆ | ecs-tasks | Inline: S3 (now bucket-scoped, see "Tightenings"), SSM Messages | โ tight after audit fix |
SanctumServices-WebTaskTaskRole-โฆ | ecs-tasks | Empty (nginx container) | โ correct โ minimal |
SanctumServices-AdminTaskTaskRole-โฆ | ecs-tasks | Empty (nginx container) | โ correct โ minimal |
SanctumServices-GuideTaskTaskRole-โฆ | ecs-tasks | Empty (nginx container) | โ correct โ minimal |
SanctumServices-ValkeyTaskTaskRole-โฆ | ecs-tasks | Empty (in-memory cache) | โ correct โ minimal |
SanctumBastion-BastionInstanceRole-โฆ | ec2 | Managed AmazonSSMManagedInstanceCore only | โ correct โ required for Session Manager |
github-actions-deploy | OIDC token.actions.githubusercontent.com pinned to repo:jophillips90/toast-guilder:environment:production | Inline sanctum-deploy: ECR push to sanctum-* repos, ECS update/run-task scoped to sanctum-app cluster, iam:PassRole scoped to SanctumServices-*, sts:AssumeRole on CDK bootstrap roles | โ tight โ trust + permissions both correctly scoped |
Amazon_EventBridge_Pipe_sanctum-sim-trigger-โฆ | pipes.amazonaws.com | AWS-auto-generated for the live EventBridge pipe | โ correct โ managed by AWS |
SanctumData-PostgresMonitoringRole-โฆ | monitoring.rds.amazonaws.com | Enhanced monitoring metric publishing | โ correct โ RDS-required |
SanctumData-PostgresRotat-โฆSecretsManagerRDSPostgreSQL-โฆ | lambda | SAR-managed for the secret rotation Lambda | โ correct โ pinned to SAR 1.1.672 |
SanctumData-InitDbFn-โฆ + SanctumData-InitDbProviderframeworkonEvent-โฆ | lambda | CDK custom-resource framework auto-generated | โ correct โ internal to DataStack |
SanctumData-AWS679f53fac002430cb0da5b7982bd2287Serv-โฆ + SanctumSecurity-AWS679f53fac002430cb0da5b7982bd2287-โฆ + SanctumCdn-AWS679f53fac002430cb0da5b7982bd2287Servi-โฆ | lambda | CDK AwsCustomResource SDK-call provider auto-generated | โ correct โ internal to stacks |
SanctumSecurity-AuditTrailLogsRole-โฆ | cloudtrail.amazonaws.com | CloudTrail-to-CloudWatch publishing | โ correct โ CloudTrail-required |
Legacy roles โ deleted in this auditโ
| Role | Last used | Permissions had | Why deleted |
|---|---|---|---|
sanctum-ec2-role | 2026-03-15 | AmazonS3FullAccess, AmazonSESFullAccess, AmazonSQSFullAccess, AmazonEC2ContainerRegistryFullAccess | Pre-Fargate EC2 instance role. ~2.5 months unused. All 4 managed-policy attachments are wildcards on global services. |
sanctum-ecs-execution-role | 2026-03-17 | AmazonECSTaskExecutionRolePolicy + inline sqs-sim-receive | Predecessor of SanctumServices-TaskExecRole. Hand-rolled before CDK ownership. |
sanctum-eventbridge-pipe-role | never (LastUsed: {}) | AmazonECS_FullAccess (broad), AmazonSQSFullAccess | The live pipe role is Amazon_EventBridge_Pipe_sanctum-sim-trigger-โฆ (AWS-auto). This was dead weight from initial setup. |
ecsTaskExecutionRole | 2026-03-17 | AmazonECSTaskExecutionRolePolicy | Default AWS-suggested name, created during initial setup. SanctumServices-TaskExecRole replaced it. |
Deletion was idempotent โ all four were detached/deleted via aws iam CLI. None are CDK-managed, so no code change required. Verified after deletion: every ECS service still 1/1 running, no missed CloudTrail entries, EventBridge pipe still firing.
Tighteningsโ
SanctumServices-ApiTaskRole and SanctumServices-GdlTaskRole S3 grantโ
Before:
actions: ['s3:*'],
resources: ['arn:aws:s3:::sanctum-*', 'arn:aws:s3:::sanctum-*/*']
Problem: the sanctum-* prefix matches not only the app data buckets (sanctum-app, sanctum-gdl, sanctum-gcl) but also every security-critical bucket in the account:
sanctum-cloudtrailโ audit trailsanctum-aws-config-โฆโ Config recordersanctum-flow-logsโ VPC flow logssanctum-s3-access-logs-โฆโ S3 server access logssanctum-alb-access-logs-โฆโ ALB access logs
If the API container were ever compromised (RCE through a vulnerable dependency, container escape, leaked task role credentials, etc.), the attacker could delete CloudTrail logs, which would also defeat post-incident investigation.
After (ApiTaskRole):
sid: 'AppDataBuckets',
actions: ['s3:*'],
resources: [
'arn:aws:s3:::sanctum-app',
'arn:aws:s3:::sanctum-app/*',
'arn:aws:s3:::sanctum-gdl',
'arn:aws:s3:::sanctum-gdl/*',
'arn:aws:s3:::sanctum-gcl',
'arn:aws:s3:::sanctum-gcl/*',
],
After (GdlTaskRole):
sid: 'GameLibraryBuckets',
actions: ['s3:*'],
resources: [
'arn:aws:s3:::sanctum-gdl',
'arn:aws:s3:::sanctum-gdl/*',
'arn:aws:s3:::sanctum-gcl',
'arn:aws:s3:::sanctum-gcl/*',
],
Decision: keep s3:* as the action set rather than enumerating verbs. The app uses PutObject, GetObject, DeleteObject, ListObjectsV2, HeadObject, and signed-URL flows (which depend on the GetObject permission of the caller). Enumerating those couples the IAM policy to S3 client implementation details โ every new SDK feature would require a policy update. Tightening the resource list achieves the actual security goal (no audit-bucket access) without the maintenance overhead.
IAM Access Analyzerโ
Archived the standing finding for github-actions-deploy (correctly-scoped OIDC role; finding was Access Analyzer doing its job conservatively rather than a misconfiguration) and added an archive rule so future renderings of the same role don't re-open the finding:
aws accessanalyzer create-archive-rule \
--analyzer-name sanctum-account-analyzer \
--rule-name github-actions-deploy-oidc \
--filter '{
"resource": {"eq": ["arn:aws:iam::357457230871:role/github-actions-deploy"]},
"principal.Federated": {"eq": ["arn:aws:iam::357457230871:oidc-provider/token.actions.githubusercontent.com"]}
}'
Active findings count after: 0.
What this audit did not touch (intentionally)โ
sanctum-adminsgroup membership โ single user (cli-user), no rotation candidate.- Access key rotation cadence โ already handled by the standing
IAM: delete or rotate stale credentials (45d+)runbook. Not in scope here. - MFA enforcement on IAM users โ both human users are CLI-only, no console password. MFA on root (separate from these users) is a behavioural task being tracked in the Sprint 13 "Stop root account usage" item.
- CDK-internal Lambda roles (
InitDbFn,AwsCustomResource, etc.) โ auto-generated by L2 constructs, follow least privilege by construction.
Followups (not blockers)โ
- Audit access keys per user โ when each user's access keys were last rotated, whether either is older than 90 days. Not a Q2 audit item but worth adding to the next sprint.
- Consider splitting
cli-userinto per-purpose roles (deploy vs read-only inspection) so day-to-day inspection work doesn't need admin. Low value at solo-developer scale, worth revisiting if the contributor count grows. - Re-audit after every major dependency change in
infra/lib/service-stack.tsโ easiest way to keep IAM tight is to keep running this script every quarter.
How this audit was performedโ
# Inventory
aws iam list-roles
aws iam list-users
aws iam list-groups
# Per-role drill-down
aws iam list-role-policies --role-name <name>
aws iam get-role-policy --role-name <name> --policy-name <policy>
aws iam list-attached-role-policies --role-name <name>
aws iam get-role --role-name <name> --query 'Role.RoleLastUsed'
# Per-group drill-down
aws iam get-group --group-name <name>
aws iam list-group-policies --group-name <name>
aws iam get-group-policy --group-name <name> --policy-name <policy>
aws iam list-attached-group-policies --group-name <name>
RoleLastUsed.LastUsedDate is the cheap heuristic for "is this role still alive" โ AWS tracks 400 days of role usage. Any role with no entry, or an entry more than 90 days old, is a candidate for deletion (verify cron/scheduled triggers first).