Skip to content

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

minitickets-dev-api-execution-role · lets ECS pull the api image, read its secrets and write its logs

Who may assume API task execution role: API task definition, Migration task definition (Principal ecs-tasks.amazonaws.com; condition aws:SourceAccount = our account (stops the confused deputy problem))

What it may do:

OnActionsPolicyWhy only this
ECR repositories, CloudWatch log groupsResource *; one AWS managed statement covers image pulls and log writesecr:GetAuthorizationTokenecr:BatchCheckLayerAvailabilityecr:GetDownloadUrlForLayerecr:BatchGetImagelogs:CreateLogStreamlogs:PutLogEventsidentity AmazonECSTaskExecutionRolePolicy (AWS managed, Resource *) createIamRolesECS 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 onsecretsmanager:GetSecretValueidentity read-secrets createIamRolesECS 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

minitickets-dev-api-task-role · what the running api code may do in AWS

Who may assume API task role: API task definition (Principal ecs-tasks.amazonaws.com; condition aws:SourceAccount = our account (stops the confused deputy problem))

What it may do:

OnActionsPolicyWhy only this
Booking-events SNS topicsns:Publishidentity send-jobs createIamRolesThe outbox relay publishes booking events. The api cannot read either queue: SNS delivers to them with the queues' own policies.
Tickets tabledynamodb:Queryidentity ReadTickets createIamRolesMy tickets is one Query on the userId partition. No Scan, and no writes: only the tickets Lambda writes tickets.
Ticket assets buckettickets/* onlys3:GetObjectidentity ReadTicketPngs createIamRolesA 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/* onlys3:GetObjectidentity ReadPosters createIamRolesThe 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/* onlys3:PutObjectif s3:x-amz-server-side-encryption = AES256identity WritePosters createIamRolesThe 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:OpenDataChannelidentity ecs-exec createIamRolesECS Exec opens a debug shell inside the api task, which can reach RDS and Valkey from inside the VPC.

Web task execution role

minitickets-dev-web-execution-role · only when entryEnabled = true · lets ECS start the web task with only the one secret it needs

Who may assume Web task execution role: Web task definition (Principal ecs-tasks.amazonaws.com; condition aws:SourceAccount = our account (stops the confused deputy problem))

What it may do:

OnActionsPolicyWhy only this
ECR repositories, CloudWatch log groupsResource *; one AWS managed statement covers image pulls and log writesecr:GetAuthorizationTokenecr:BatchCheckLayerAvailabilityecr:GetDownloadUrlForLayerecr:BatchGetImagelogs:CreateLogStreamlogs:PutLogEventsidentity AmazonECSTaskExecutionRolePolicy (AWS managed, Resource *) createWebIamRoleECS 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 secretsecretsmanager:GetSecretValueidentity read-revalidate-secret createWebIamRoleWeb needs exactly one secret. It cannot read the database password or the Redis URL.

Worker Lambda execution role

minitickets-dev-worker-role · only when computeEnabled = true · what the email worker may touch: one queue, one table, SES, one log group

Who may assume Worker Lambda execution role: Worker Lambda (Principal lambda.amazonaws.com; only someone with iam:PassRole on it can attach it to a function)

What it may do:

OnActionsPolicyWhy only this
Jobs SQS queuesqs:ReceiveMessagesqs:DeleteMessagesqs:ChangeMessageVisibilitysqs:GetQueueAttributesidentity ReadJobsQueue createWorkerRoleThe 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 tabledynamodb:GetItemdynamodb:PutItemidentity ProcessedJobsTable createWorkerRoleisDone 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 ARNses:SendEmailif ses:FromAddress = no-reply@minitickets.theversehub.inidentity SendBookingEmails createWorkerRoleThe policy restricts the sender to our configured address; it does not constrain email content.
CloudWatch log groups/minitickets/dev/worker:* onlylogs:CreateLogStreamlogs:PutLogEventsidentity WriteWorkerLogs createWorkerRoleNo logs:CreateLogGroup: Pulumi creates the group with a 7-day retention.

Tickets Lambda execution role

minitickets-dev-tickets-role · only when computeEnabled = true · what the tickets Lambda may touch

Who may assume Tickets Lambda execution role: Tickets Lambda (Principal lambda.amazonaws.com)

What it may do:

OnActionsPolicyWhy only this
Tickets SQS queuesqs:ReceiveMessagesqs:DeleteMessagesqs:GetQueueAttributesidentity createTicketRoleConsume the tickets queue through the event source mapping.
Tickets tabledynamodb:GetItemdynamodb:PutItemdynamodb:UpdateItemidentity createTicketRoleReserve, write and complete the ticket items of one booking.
Ticket assets buckettickets/* onlys3:PutObjectidentity createTicketRoleWrite the QR PNGs. It cannot read or touch posters.
Ticket signing secretsecretsmanager:GetSecretValueidentity createTicketRoleRead the HMAC key that signs tickets. The api never gets this key.
CloudWatch log groups/minitickets/dev/tickets:* onlylogs:CreateLogStreamlogs:PutLogEventsidentity createTicketRoleWrite its own log lines, nowhere else.

GitHub publish role

minitickets-dev-gh-publish · may push images to our three ECR repositories, nothing else

Who may assume GitHub publish role: CD publish job (Principal Federated = the GitHub OIDC provider; aud = sts.amazonaws.com; sub = repo:maheshnd@63289193/minitickets@1412723955:ref:refs/heads/main (StringEquals))

What it may do:

OnActionsPolicyWhy only this
ECR repositoriesResource * (the login is registry-wide; AWS accepts only *)ecr:GetAuthorizationTokenidentity EcrLogin publishPolicyDocumentdocker login needs a registry token. It grants nothing by itself.
ECR repositoriesthe 3 minitickets-dev repositories onlyecr:BatchCheckLayerAvailabilityecr:BatchGetImageecr:GetDownloadUrlForLayerecr:InitiateLayerUploadecr:UploadLayerPartecr:CompleteLayerUploadecr:PutImageidentity PushPullMiniticketsRepos publishPolicyDocumentExactly what docker push and the 'does this tag exist?' check need. No delete, no lifecycle changes, no ECS or IAM.

GitHub deploy role

minitickets-dev-gh-deploy · what an approved deploy job may change in AWS

Who may assume GitHub deploy role: CD deploy job (Principal Federated = the GitHub OIDC provider; aud = sts.amazonaws.com; sub = repo:maheshnd@63289193/minitickets@1412723955:environment:dev (StringEquals))

What it may do:

OnActionsPolicyWhy 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 projectsec2:*elasticloadbalancing:*ecs:*rds:*elasticache:*cloudfront:*acm:*logs:*servicediscovery:*budgets:*kms:DescribeKeyidentity StackServices createCicdPulumi 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 createCicdPulumi 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-* onlyiam:CreateRoleiam:DeleteRoleiam:UpdateRoleiam:UpdateRoleDescriptioniam:UpdateAssumeRolePolicyiam:TagRoleiam:UntagRoleiam:GetRolePolicyiam:PutRolePolicyiam:DeleteRolePolicyiam:DetachRolePolicyiam:ListInstanceProfilesForRoleidentity ManageOurRoles createCicdPulumi 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/AmazonECSTaskExecutionRolePolicyidentity AttachOnlyEcsExecutionPolicy createCicdThe 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 createCicdRegistering 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:ListRoleTagsidentity ReadIam createCicdPulumi 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.comidentity ServiceLinkedRoles createCicdA 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 onlyiam:GetOpenIDConnectProvideriam:CreateOpenIDConnectProvideriam:DeleteOpenIDConnectProvideriam:TagOpenIDConnectProvideriam:UntagOpenIDConnectProvideriam:ListOpenIDConnectProviderTagsiam:UpdateOpenIDConnectProviderThumbprintiam:AddClientIDToOpenIDConnectProvideriam:RemoveClientIDFromOpenIDConnectProvideridentity GithubOidcProvider createCicdThe OIDC provider is part of the stack, so Pulumi manages it.
ECR repositoriesthe 3 repository ARNsecr:*identity OurEcrRepos createCicdPulumi owns the repositories; cd.yml checks tags and adds deployed-<sha>.
Release parametersonly /minitickets/dev/image-tag and /minitickets/dev/deployed-commitssm:GetParameterssm:GetParametersssm:PutParameterssm:DeleteParameterssm:AddTagsToResourcessm:RemoveTagsFromResourcessm:ListTagsForResourceidentity OurReleaseParameters createCicdCD 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 onlyssm:GetParametersByPathidentity ReadOurParameterPath createCicdPulumi 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 toossm:DescribeParametersidentity ListParameterNames createCicdListing 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 regionsqs:*dynamodb:*lambda:*ses:*identity JobPipeline createCicdPulumi 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 regionlambda:UpdateEventSourceMappinglambda:DeleteEventSourceMappinglambda:ListTagslambda:TagResourcelambda:UntagResourceidentity LambdaMappingsLifecycle createCicdPulumi 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 endpointlambda:GetEventSourceMappingidentity LambdaMappingsRead createCicdThe 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 herelambda:CreateEventSourceMappinglambda:ListEventSourceMappingsidentity LambdaMappingsCreateList createCicdA 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-devsns:*cloudwatch:*identity Observability createCicdPulumi 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 toocloudwatch:DescribeAlarmsidentity ReadAlarms createCicdPulumi lists alarm settings with a separate account-wide read permission. It grants no extra alarm writes.
Ticket assets bucketthe bucket and its objectss3:*identity TicketAssets createCicdPulumi 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 resourcesDeny *if aws:RequestedRegion != us-east-1identity DenyOtherRegions createCicdAn 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.

WhoOnActionsPolicyWhy only this
Worker Lambda, Tickets LambdaECR repositoriesthe minitickets-dev-worker repository onlyecr:BatchGetImageecr:GetDownloadUrlForLayerif Principal lambda.amazonaws.com; aws:SourceArn like arn:aws:lambda:<region>:<account>:function:minitickets-dev-*resource LambdaPullWorkerImage allowLambdaPullLambda 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 topicJobs SQS queuesqs:SendMessageif Principal sns.amazonaws.com; aws:SourceArn = the booking-events topic; aws:SourceAccount = our accountresource minitickets-dev-booking-events-email queue policy createBookingEventsThis policy on the jobs queue lets only our topic deliver email jobs. The tickets queue has its own separate policy.
Booking-events SNS topicTickets SQS queuesqs:SendMessageif Principal sns.amazonaws.com; aws:SourceArn = the booking-events topic; aws:SourceAccount = our accountresource minitickets-dev-booking-events-tickets queue policy createBookingEventsThis policy on the tickets queue lets only our topic deliver ticket jobs. A policy on the jobs queue would not allow this delivery.
CloudWatch alarmsAlerts SNS topicsns:Publishif Principal cloudwatch.amazonaws.com; aws:SourceArn like alarm:minitickets-dev-*; aws:SourceAccount = our accountresource OurCloudWatchAlarmsMayPublish createAlertTopicReplaces the default topic policy: only our own alarms may publish alerts.
All principalsTicket assets bucketthe ticket-assets bucket ARN and every object ARN under itDeny s3:*if aws:SecureTransport = falseresource minitickets-dev-ticket-assets-tls bucket policy createTicketStorageEvery 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/