Skip to main content

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)โ€‹

UserUsed forHas passwordMFAAccess keysVerdict
cli-userLocal AWS CLI work only (no console)NoNoYesโœ… correct โ€” member of sanctum-admins
john-adminAWS console sign-in (the human-facing admin, replaces root for day-to-day console work)YesYesNoโœ… added 2026-05-27 โ€” member of sanctum-admins
toast-guilder-api-devLocal docker-compose talking to dev bucketsNoNoYesโœ… 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)โ€‹

GroupMembersPermissionsVerdict
sanctum-adminscli-userAdministratorAccess (managed)โœ… correct โ€” break-glass + bootstrap, single member
toast-guilder-app-devtoast-guilder-api-devInline 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)โ€‹

RoleTrusted byPermissions summaryVerdict
SanctumServices-TaskExecRole-โ€ฆecs-tasksManaged AmazonECSTaskExecutionRolePolicy + inline (secrets-by-ARN, ECR pull-by-repo, logs-by-group)โœ… tight
SanctumServices-ApiTaskRole-โ€ฆecs-tasksInline: S3 (now bucket-scoped, see "Tightenings"), Rekognition moderation, CloudWatch Insights queries, SSM Messages for ECS Execโœ… tight after audit fix
SanctumServices-GdlTaskRole-โ€ฆecs-tasksInline: S3 (now bucket-scoped, see "Tightenings"), SSM Messagesโœ… tight after audit fix
SanctumServices-WebTaskTaskRole-โ€ฆecs-tasksEmpty (nginx container)โœ… correct โ€” minimal
SanctumServices-AdminTaskTaskRole-โ€ฆecs-tasksEmpty (nginx container)โœ… correct โ€” minimal
SanctumServices-GuideTaskTaskRole-โ€ฆecs-tasksEmpty (nginx container)โœ… correct โ€” minimal
SanctumServices-ValkeyTaskTaskRole-โ€ฆecs-tasksEmpty (in-memory cache)โœ… correct โ€” minimal
SanctumBastion-BastionInstanceRole-โ€ฆec2Managed AmazonSSMManagedInstanceCore onlyโœ… correct โ€” required for Session Manager
github-actions-deployOIDC token.actions.githubusercontent.com pinned to repo:jophillips90/toast-guilder:environment:productionInline 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.comAWS-auto-generated for the live EventBridge pipeโœ… correct โ€” managed by AWS
SanctumData-PostgresMonitoringRole-โ€ฆmonitoring.rds.amazonaws.comEnhanced monitoring metric publishingโœ… correct โ€” RDS-required
SanctumData-PostgresRotat-โ€ฆSecretsManagerRDSPostgreSQL-โ€ฆlambdaSAR-managed for the secret rotation Lambdaโœ… correct โ€” pinned to SAR 1.1.672
SanctumData-InitDbFn-โ€ฆ + SanctumData-InitDbProviderframeworkonEvent-โ€ฆlambdaCDK custom-resource framework auto-generatedโœ… correct โ€” internal to DataStack
SanctumData-AWS679f53fac002430cb0da5b7982bd2287Serv-โ€ฆ + SanctumSecurity-AWS679f53fac002430cb0da5b7982bd2287-โ€ฆ + SanctumCdn-AWS679f53fac002430cb0da5b7982bd2287Servi-โ€ฆlambdaCDK AwsCustomResource SDK-call provider auto-generatedโœ… correct โ€” internal to stacks
SanctumSecurity-AuditTrailLogsRole-โ€ฆcloudtrail.amazonaws.comCloudTrail-to-CloudWatch publishingโœ… correct โ€” CloudTrail-required

Legacy roles โ€” deleted in this auditโ€‹

RoleLast usedPermissions hadWhy deleted
sanctum-ec2-role2026-03-15AmazonS3FullAccess, AmazonSESFullAccess, AmazonSQSFullAccess, AmazonEC2ContainerRegistryFullAccessPre-Fargate EC2 instance role. ~2.5 months unused. All 4 managed-policy attachments are wildcards on global services.
sanctum-ecs-execution-role2026-03-17AmazonECSTaskExecutionRolePolicy + inline sqs-sim-receivePredecessor of SanctumServices-TaskExecRole. Hand-rolled before CDK ownership.
sanctum-eventbridge-pipe-rolenever (LastUsed: {})AmazonECS_FullAccess (broad), AmazonSQSFullAccessThe live pipe role is Amazon_EventBridge_Pipe_sanctum-sim-trigger-โ€ฆ (AWS-auto). This was dead weight from initial setup.
ecsTaskExecutionRole2026-03-17AmazonECSTaskExecutionRolePolicyDefault 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 trail
  • sanctum-aws-config-โ€ฆ โ€” Config recorder
  • sanctum-flow-logs โ€” VPC flow logs
  • sanctum-s3-access-logs-โ€ฆ โ€” S3 server access logs
  • sanctum-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-admins group 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)โ€‹

  1. 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.
  2. Consider splitting cli-user into 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.
  3. 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).