Permissions matrix
Every IAM role in MiniTickets, with the actions and scope of each policy statement. For each role you see:
- Who may assume it: its trust policy. A role does nothing until someone trusted assumes it.
- What it may do: its permission statements (identity policies), each with the reason why only this permission is needed.
At the end come the resource policies, with both caller and target. A resource policy sits on the resource itself (a queue, a topic, a repository or a bucket). It can allow a caller or deny matching requests. The bucket’s HTTP Deny applies to all principals, meaning every caller.
Shared statements list their known consumers in one row: the API and migration share an
execution-role trust statement; both Lambdas share an ECR repository statement. A listed map
piece is a link, while scope gives
the real ARN limit; Resource * also covers resources outside MiniTickets.
The api and web tasks have two kinds of role. ECS uses the execution role to start a task: pull the image, read the secrets, write the logs. The app’s own code runs with the task role. The web task has no task role at all, because its code calls no AWS API.
The tables are generated from apps/book/src/data, checked against the IAM code in infra/src.
API task execution role
Who may assume API task execution role: API task definition, Migration task definition
What it may do:
| On | Actions | Policy | Why only this |
|---|---|---|---|
| ECR repositories, CloudWatch log groupsResource *; one AWS managed statement covers image pulls and log writes | ecr:GetAuthorizationTokenecr:BatchCheckLayerAvailabilityecr:GetDownloadUrlForLayerecr:BatchGetImagelogs:CreateLogStreamlogs:PutLogEvents | identity AmazonECSTaskExecutionRolePolicy (AWS managed, Resource *) createIamRoles | ECS pulls the image before startup, then the awslogs driver writes the container's output. The AWS managed statement applies to all repositories and log groups, not only ours. |
| RDS master password secret, Redis URL secret, Revalidate secretexactly these secret ARNs, never *; the revalidate secret only with entry on | secretsmanager:GetSecretValue | identity read-secrets createIamRoles | ECS puts DB_USER, DB_PASSWORD, REDIS_URL and REVALIDATE_SECRET into the container at start. No kms:Decrypt is needed: the secrets use the AWS managed key aws/secretsmanager. |
API task role
Who may assume API task role: API task definition
What it may do:
| On | Actions | Policy | Why only this |
|---|---|---|---|
| Booking-events SNS topic | sns:Publish | identity send-jobs createIamRoles | The outbox relay publishes booking events. The api cannot read either queue: SNS delivers to them with the queues' own policies. |
| Tickets table | dynamodb:Query | identity ReadTickets createIamRoles | My tickets is one Query on the userId partition. No Scan, and no writes: only the tickets Lambda writes tickets. |
| Ticket assets buckettickets/* only | s3:GetObject | identity ReadTicketPngs createIamRoles | A presigned ticket GET works with the signer's permissions, so the api role must be allowed to read the ticket PNG it signs. |
| Ticket assets bucketposters/* and poster-uploads/* only | s3:GetObject | identity ReadPosters createIamRoles | The api reads staged uploads to check them before publishing, and reads poster objects. This separate statement grants no ticket writes. |
| Ticket assets bucketposters/* and poster-uploads/* only | s3:PutObjectif s3:x-amz-server-side-encryption = AES256 | identity WritePosters createIamRoles | The browser's presigned poster PUT and the copy to posters/ run with this role. It can never overwrite tickets/. |
| API ECS serviceResource * (these actions cannot be limited to one resource) | ssmmessages:CreateControlChannelssmmessages:CreateDataChannelssmmessages:OpenControlChannelssmmessages:OpenDataChannel | identity ecs-exec createIamRoles | ECS Exec opens a debug shell inside the api task, which can reach RDS and Valkey from inside the VPC. |
Web task execution role
Who may assume Web task execution role: Web task definition
What it may do:
| On | Actions | Policy | Why only this |
|---|---|---|---|
| ECR repositories, CloudWatch log groupsResource *; one AWS managed statement covers image pulls and log writes | ecr:GetAuthorizationTokenecr:BatchCheckLayerAvailabilityecr:GetDownloadUrlForLayerecr:BatchGetImagelogs:CreateLogStreamlogs:PutLogEvents | identity AmazonECSTaskExecutionRolePolicy (AWS managed, Resource *) createWebIamRole | ECS pulls the web image and the awslogs driver writes its output. The AWS managed statement applies to all repositories and log groups, not only ours. |
| Revalidate secret | secretsmanager:GetSecretValue | identity read-revalidate-secret createWebIamRole | Web needs exactly one secret. It cannot read the database password or the Redis URL. |
Worker Lambda execution role
Who may assume Worker Lambda execution role: Worker Lambda
What it may do:
| On | Actions | Policy | Why only this |
|---|---|---|---|
| Jobs SQS queue | sqs:ReceiveMessagesqs:DeleteMessagesqs:ChangeMessageVisibilitysqs:GetQueueAttributes | identity ReadJobsQueue createWorkerRole | The event source mapping receives and deletes with this role; the code's backoff changes the visibility. No SendMessage, no DLQ: SQS itself moves failed messages. |
| Processed-jobs table | dynamodb:GetItemdynamodb:PutItem | identity ProcessedJobsTable createWorkerRole | isDone is a GetItem and markDone a conditional PutItem. No Scan, Query or Delete: TTL deletes for free. |
| SES email identitiesour domain identity ARN and the test-address identity ARN | ses:SendEmailif ses:FromAddress = no-reply@minitickets.theversehub.in | identity SendBookingEmails createWorkerRole | The policy restricts the sender to our configured address; it does not constrain email content. |
| CloudWatch log groups/minitickets/dev/worker:* only | logs:CreateLogStreamlogs:PutLogEvents | identity WriteWorkerLogs createWorkerRole | No logs:CreateLogGroup: Pulumi creates the group with a 7-day retention. |
Tickets Lambda execution role
Who may assume Tickets Lambda execution role: Tickets Lambda
What it may do:
| On | Actions | Policy | Why only this |
|---|---|---|---|
| Tickets SQS queue | sqs:ReceiveMessagesqs:DeleteMessagesqs:GetQueueAttributes | identity createTicketRole | Consume the tickets queue through the event source mapping. |
| Tickets table | dynamodb:GetItemdynamodb:PutItemdynamodb:UpdateItem | identity createTicketRole | Reserve, write and complete the ticket items of one booking. |
| Ticket assets buckettickets/* only | s3:PutObject | identity createTicketRole | Write the QR PNGs. It cannot read or touch posters. |
| Ticket signing secret | secretsmanager:GetSecretValue | identity createTicketRole | Read the HMAC key that signs tickets. The api never gets this key. |
| CloudWatch log groups/minitickets/dev/tickets:* only | logs:CreateLogStreamlogs:PutLogEvents | identity createTicketRole | Write its own log lines, nowhere else. |
GitHub publish role
Who may assume GitHub publish role: CD publish job
What it may do:
| On | Actions | Policy | Why only this |
|---|---|---|---|
| ECR repositoriesResource * (the login is registry-wide; AWS accepts only *) | ecr:GetAuthorizationToken | identity EcrLogin publishPolicyDocument | docker login needs a registry token. It grants nothing by itself. |
| ECR repositoriesthe 3 minitickets-dev repositories only | ecr:BatchCheckLayerAvailabilityecr:BatchGetImageecr:GetDownloadUrlForLayerecr:InitiateLayerUploadecr:UploadLayerPartecr:CompleteLayerUploadecr:PutImage | identity PushPullMiniticketsRepos publishPolicyDocument | Exactly what docker push and the 'does this tag exist?' check need. No delete, no lifecycle changes, no ECS or IAM. |
GitHub deploy role
Who may assume GitHub deploy role: CD deploy job
What it may do:
| On | Actions | Policy | Why only this |
|---|---|---|---|
| AWS account and region, Monthly cost budget, VPC, Public subnets, Private subnets, Internet gateway, Public route table, Private route table, ALB security group, Web security group, API security group, Database security group, Cache security group, RDS Postgres, ElastiCache Valkey, ECS cluster, API task definition, API ECS service, Web task definition, Web ECS service, ECS Service Connect, Migration task definition, CloudWatch log groups, Saved Logs Insights queries, Application Load Balancer, API target group, Web target group, HTTPS listener, HTTP listener with the origin header, ACM certificate, CloudFront distributionResource *; covers these services throughout the account, including other projects | ec2:*elasticloadbalancing:*ecs:*rds:*elasticache:*cloudfront:*acm:*logs:*servicediscovery:*budgets:*kms:DescribeKey | identity StackServices createCicd | Pulumi creates, changes and deletes resources before it knows their ids. This broad statement also permits changes outside MiniTickets. The region deny limits it; approval protection depends on GitHub's separately configured environment settings. |
| RDS master password secret, Redis URL secret, Revalidate secret, Ticket signing secretsecret:minitickets/* and secret:rds!* | secretsmanager:* | identity OurSecrets createCicd | Pulumi manages our secrets, and RDS creates its master secret with the caller's permissions. It can also read them: be aware. |
| API task execution role, API task role, Web task execution role, Worker Lambda execution role, Tickets Lambda execution role, GitHub publish role, GitHub deploy rolerole/minitickets-dev-* only | iam:CreateRoleiam:DeleteRoleiam:UpdateRoleiam:UpdateRoleDescriptioniam:UpdateAssumeRolePolicyiam:TagRoleiam:UntagRoleiam:GetRolePolicyiam:PutRolePolicyiam:DeleteRolePolicyiam:DetachRolePolicyiam:ListInstanceProfilesForRole | identity ManageOurRoles createCicd | Pulumi manages our roles. PutRolePolicy cannot be limited by content: a permissions boundary would be the proper ceiling (not built in dev). |
| API task execution role, API task role, Web task execution role, Worker Lambda execution role, Tickets Lambda execution role, GitHub publish role, GitHub deploy rolerole/minitickets-dev-* | iam:AttachRolePolicyif iam:PolicyARN = arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy | identity AttachOnlyEcsExecutionPolicy createCicd | The api and web execution roles use this attachment, but the policy permits it on any matching role. The condition prevents attaching AdministratorAccess. |
| API task execution role, API task role, Web task execution role, Worker Lambda execution role, Tickets Lambda execution role, GitHub publish role, GitHub deploy rolerole/minitickets-dev-* | iam:PassRoleif iam:PassedToService in [ecs-tasks.amazonaws.com, lambda.amazonaws.com] | identity PassOurRolesToEcsTasksAndLambda createCicd | Registering a task definition, running the migration and creating a Lambda all hand a role to a service. The policy covers any matching role, including the publish and deploy roles. |
| API task execution role, API task role, Web task execution role, Worker Lambda execution role, Tickets Lambda execution role, GitHub publish role, GitHub deploy roleResource * (read-only) | iam:GetRoleiam:ListRolePoliciesiam:ListAttachedRolePoliciesiam:ListRoleTags | identity ReadIam createCicd | Pulumi reads the roles it manages; ECS and RDS look up their service-linked roles. |
| AWS account and regionrole/aws-service-role/* | iam:CreateServiceLinkedRoleif iam:AWSServiceName in [ecs, elasticloadbalancing, rds, elasticache].amazonaws.com | identity ServiceLinkedRoles createCicd | A new account may not have these AWS-owned roles yet; usually they already exist. |
| GitHub OIDC identity provideroidc-provider/token.actions.githubusercontent.com in our account only | iam:GetOpenIDConnectProvideriam:CreateOpenIDConnectProvideriam:DeleteOpenIDConnectProvideriam:TagOpenIDConnectProvideriam:UntagOpenIDConnectProvideriam:ListOpenIDConnectProviderTagsiam:UpdateOpenIDConnectProviderThumbprintiam:AddClientIDToOpenIDConnectProvideriam:RemoveClientIDFromOpenIDConnectProvider | identity GithubOidcProvider createCicd | The OIDC provider is part of the stack, so Pulumi manages it. |
| ECR repositoriesthe 3 repository ARNs | ecr:* | identity OurEcrRepos createCicd | Pulumi owns the repositories; cd.yml checks tags and adds deployed-<sha>. |
| Release parametersonly /minitickets/dev/image-tag and /minitickets/dev/deployed-commit | ssm:GetParameterssm:GetParametersssm:PutParameterssm:DeleteParameterssm:AddTagsToResourcessm:RemoveTagsFromResourcessm:ListTagsForResource | identity OurReleaseParameters createCicd | CD reads and writes the release values, and Pulumi manages the two parameters and their tags. This statement grants no access to other parameters. |
| Release parametersparameter/minitickets/dev path ARN only | ssm:GetParametersByPath | identity ReadOurParameterPath createCicd | Pulumi reads the image tag by its path. That call needs the parent path ARN, not just the two parameter ARNs. |
| Release parametersResource *; may list parameter metadata outside our path too | ssm:DescribeParameters | identity ListParameterNames createCicd | Listing parameter names needs a separate account-wide read permission. It does not return secret parameter values. |
| Jobs SQS queue, Jobs dead-letter queue, Tickets SQS queue, Processed-jobs table, Tickets table, Worker Lambda, Tickets Lambda, SES email identitiesqueues, tables and functions named minitickets-dev-*; SES identity/* in our account and region | sqs:*dynamodb:*lambda:*ses:* | identity JobPipeline createCicd | Pulumi owns the job pipeline. Event source mappings have separate explicit lifecycle permissions, including the reads used while waiting. |
| Worker Lambda, Tickets Lambdaarn:aws:lambda:us-east-1:<account>:event-source-mapping:*; all mappings in this account and region | lambda:UpdateEventSourceMappinglambda:DeleteEventSourceMappinglambda:ListTagslambda:TagResourcelambda:UntagResource | identity LambdaMappingsLifecycle createCicd | Pulumi updates/deletes mappings and reads/manages their tags. AWS assigns each UUID, so mapping ARNs cannot use our function-name prefix. These writes remain scoped; the deletion waiter's Get has a separate read grant. |
| Worker Lambda, Tickets LambdaResource *; reads any mapping in the account through the us-east-1 endpoint | lambda:GetEventSourceMapping | identity LambdaMappingsRead createCicd | The failed compute-off CD log shows Lambda checking GetEventSourceMapping against resource * while Pulumi waits after deletion. An ARN-scoped Get does not cover that request. This specific read permits the waiter to reach its terminal not-found response; it grants no mapping writes. |
| Worker Lambda, Tickets LambdaResource *; event source mappings are not limited by function name here | lambda:CreateEventSourceMappinglambda:ListEventSourceMappings | identity LambdaMappingsCreateList createCicd | A new mapping has no ARN yet, and listing mappings is an account-wide operation. The function role controls which queue an active mapping can read. |
| Alerts SNS topic, Booking-events SNS topic, CloudWatch alarms, CloudWatch dashboardSNS minitickets-dev-*; alarm:minitickets-dev-*; dashboard/minitickets-dev | sns:*cloudwatch:* | identity Observability createCicd | Pulumi owns our topics, alarms and dashboard. The SNS pattern also covers the booking-events topic and subscriptions under either topic. |
| CloudWatch alarmsResource *; may read alarm settings outside our prefix too | cloudwatch:DescribeAlarms | identity ReadAlarms createCicd | Pulumi lists alarm settings with a separate account-wide read permission. It grants no extra alarm writes. |
| Ticket assets bucketthe bucket and its objects | s3:* | identity TicketAssets createCicd | Pulumi manages the bucket and its settings. |
| AWS account and region, Monthly cost budget, VPC, Public subnets, Private subnets, Internet gateway, Public route table, Private route table, ALB security group, Web security group, API security group, Database security group, Cache security group, RDS Postgres, ElastiCache Valkey, ECS cluster, API task definition, API ECS service, Web task definition, Web ECS service, ECS Service Connect, Migration task definition, CloudWatch log groups, Saved Logs Insights queries, Application Load Balancer, API target group, Web target group, HTTPS listener, HTTP listener with the origin header, ACM certificate, CloudFront distribution, API task execution role, API task role, Web task execution role, Worker Lambda execution role, Tickets Lambda execution role, GitHub publish role, GitHub deploy role, RDS master password secret, Redis URL secret, Revalidate secret, Ticket signing secret, Processed-jobs table, Tickets table, Ticket assets bucket, ECR repositories, Booking-events SNS topic, Jobs SQS queue, Jobs dead-letter queue, Tickets SQS queue, Worker Lambda, Tickets Lambda, SES email identities, GitHub OIDC identity provider, Release parameters, CloudWatch alarms, Alerts SNS topic, CloudWatch dashboardResource *; all actions on all resources | Deny *if aws:RequestedRegion != us-east-1 | identity DenyOtherRegions createCicd | An explicit Deny beats every Allow: requests to other region endpoints are refused. Global services such as IAM and CloudFront use us-east-1 endpoints and pass this condition. |
Resource policies
These statements sit on the resource itself. They name allowed callers or deny matching requests. Read them together with identity policies; an explicit Deny wins.
| Who | On | Actions | Policy | Why only this |
|---|---|---|---|---|
| Worker Lambda, Tickets Lambda | ECR repositoriesthe minitickets-dev-worker repository only | ecr:BatchGetImageecr:GetDownloadUrlForLayerif Principal lambda.amazonaws.com; aws:SourceArn like arn:aws:lambda:<region>:<account>:function:minitickets-dev-* | resource LambdaPullWorkerImage allowLambdaPull | Lambda pulls an image with the repository's permission, not with an IAM role. The condition stops Lambda working for another account from pulling our image. |
| Booking-events SNS topic | Jobs SQS queue | sqs:SendMessageif Principal sns.amazonaws.com; aws:SourceArn = the booking-events topic; aws:SourceAccount = our account | resource minitickets-dev-booking-events-email queue policy createBookingEvents | This policy on the jobs queue lets only our topic deliver email jobs. The tickets queue has its own separate policy. |
| Booking-events SNS topic | Tickets SQS queue | sqs:SendMessageif Principal sns.amazonaws.com; aws:SourceArn = the booking-events topic; aws:SourceAccount = our account | resource minitickets-dev-booking-events-tickets queue policy createBookingEvents | This policy on the tickets queue lets only our topic deliver ticket jobs. A policy on the jobs queue would not allow this delivery. |
| CloudWatch alarms | Alerts SNS topic | sns:Publishif Principal cloudwatch.amazonaws.com; aws:SourceArn like alarm:minitickets-dev-*; aws:SourceAccount = our account | resource OurCloudWatchAlarmsMayPublish createAlertTopic | Replaces the default topic policy: only our own alarms may publish alerts. |
| All principals | Ticket assets bucketthe ticket-assets bucket ARN and every object ARN under it | Deny s3:*if aws:SecureTransport = false | resource minitickets-dev-ticket-assets-tls bucket policy createTicketStorage | Every caller, including anonymous requests, is refused over plain HTTP. This deny overrides an identity grant or a presigned URL; use HTTPS. |
Read this book online: https://book.theversehub.in/