Skip to content

Container Permissions

Alex English edited this page Sep 18, 2026 · 1 revision

Container Permissions

Your project's container runs on the incubator as an ECS task. When your application code calls an AWS API — reading a file from S3, sending a message to a queue, looking up a user in Cognito — it authenticates as an IAM role that the incubator creates for it.

This page explains which role that is, what it can already do, and how to get it a permission it does not have yet.

The two roles, and which one you want

Every container gets two IAM roles, and they are not interchangeable.

Task role Execution role
Named ecs-container-<project>-<app>-<env> ecs-execution-<project>-<app>-<env>
Used by your application code, at runtime the ECS agent, before your code starts
Does whatever your application asks AWS to do pulls the image, creates the log stream, reads startup secrets
You want this one not this one

Application permissions go on the task role. If your code calls an AWS API, it is the task role that has to allow it. The execution role is infrastructure plumbing: it exists so ECS can start your container at all, it is scoped to your project's ECR repository, log group and SSM parameters, and it is not an extension point for application code.

A name that will mislead you. There is a role in this account called incubator-prod-ecs-task-role. Despite the name it is an execution role, not a task role — it is the shared, pre-2026 role that every container used before each got its own. No running container uses it any more. It is kept only until the module's use_own_execution_role escape hatch is removed, and both will be deleted together. If you are reading role names in the IAM console, the real task roles are the ecs-container-* ones.

What your task role can already do

Before asking for a permission, check whether you have it. Every container's task role is granted these, with no request and no configuration:

  • aws ecs execute-command — an interactive shell into a running task, via the four ssmmessages actions.
  • S3 — ListBucket, GetObject, PutObject and DeleteObject on any bucket tagged with your project. See the walkthrough below; there is one setup step.
  • Cognito — AdminGetUser, AdminCreateUser, AdminAddUserToGroup and AdminDeleteUser on any user pool tagged with your project.

All three are scoped by the project tag, so each project reaches its own resources and no others. You do not need to ask for any of them.

About Cognito specifically

Those four Admin* operations are the only Cognito calls that need IAM permission at all. The rest of the sign-in surface — SignUp, ConfirmSignUp, ResendConfirmationCode, InitiateAuth, RespondToAuthChallenge, GetUser, GlobalSignOut, ForgotPassword, ConfirmForgotPassword — are unauthenticated APIs that authorize against the end user's own credentials. They work with no role permissions and cannot be restricted by adding any. If one of those is failing, the cause is somewhere other than IAM.

Walkthrough: giving your container an S3 bucket

This is the most common request, and it needs no policy change — the grant above already covers it. What it needs is for the bucket to be tagged and opted in. Three things, all in your project's directory under terraform/projects/<project>/:

resource "aws_s3_bucket" "uploads" {
  bucket = "hfla-myproject-uploads"

  tags = {
    project = local.project_name
  }
}

resource "aws_s3_bucket_abac" "uploads" {
  bucket = aws_s3_bucket.uploads.id

  abac_status {
    status = "Enabled"
  }
}
  1. The bucket.
  2. The project tag, whose value is the HfLA project name — never an application or repository name. The permitted values are fixed; see the decision record linked at the bottom of this page.
  3. aws_s3_bucket_abac with status Enabled.

Step 3 is not optional, and skipping it fails silently. S3 does not evaluate tag conditions against a bucket that has not opted in to attribute-based access control. Leave it out and the policy still looks correct, the tag is still there, and your container gets AccessDenied with nothing to explain why.

Enabling ABAC also changes how that bucket's tags are managed — PutBucketTagging gives way to TagResource. Terraform handles this itself and the CI roles already hold the permissions, so there is nothing for you to do about it.

Asking for a permission you do not have

If your application needs a service not in the list above, that is a real request and the answer is a change to the shared container module.

What happens

The task role's policy lives in one place — terraform/modules/container/main.tf, in aws_iam_policy.container_policy — and it is shared by every container on the incubator. A new permission is a new statement there, scoped by the project tag so that adding it for your project does not hand the same access to everyone else's.

A statement granting a container access to its own SQS queues looks like this:

{
  Sid    = "ProjectQueueAccess"
  Effect = "Allow"
  Action = [
    "sqs:SendMessage",
    "sqs:ReceiveMessage",
    "sqs:DeleteMessage",
  ]
  Resource = "arn:aws:sqs:us-west-2:035866691871:*"
  Condition = {
    StringEquals = { "aws:ResourceTag/project" = var.project_name }
  }
}

The shape is always the same: a wildcard resource narrowed by the project tag, rather than a list of named resources. That is deliberate — it means a queue you create next month is covered the day you tag it, with no further policy change.

Two things that can make the answer "not like that"

  • Not every AWS service supports this. A service has to offer both resource-level permissions and the aws:ResourceTag condition key. Most do; some do not, and some support them on only a subset of their actions. The authoritative source is the AWS Service Authorization Reference — find your service, find the action, and check its "Resource types" and "Condition keys" columns before assuming.
  • Some actions cannot be scoped at all. ecr:GetAuthorizationToken is the example we already live with: it is account-level, supports no resource-level permissions and no conditions, and so sits at Resource: "*" in the execution role with a comment saying why. If the action you need turns out to be one of these, say so in your request — the decision then becomes whether the unscoped grant is acceptable, which is a conversation rather than a policy edit.

How to ask

Open an issue on hackforla/incubator using Blank Issue Form with No Dependency.

The template picker on this repository is cluttered with templates inherited from the Hack for LA website repo — accessibility audits, team rosters, project logos — none of which apply here. There is also a Blank issue entry that looks right and is a superseded version of the form. Pick Blank Issue Form with No Dependency; ignore the rest. Clearing them out is tracked as #146.

Include:

  • Which project and which container — project name, application type and environment, which together give the role name.
  • Which AWS service and which actions, as specific as you can be. "Read and write objects in our own bucket" is enough to start from; a blanket "S3 access" is not.
  • What the application does with it, in a sentence. This is what makes it possible to tell whether a narrower grant would do.
  • Whether the resources are tagged with your project, or need to be created.

Related

  • Container module documentation — the generated reference for the module, including its inputs, outputs and the full S3 and Cognito notes summarised above.
  • DR-Machine-to-machine-IAM-scoping — the decision record behind all of this: what the project tag means, which resource types carry it, the permitted values, and the exceptions.
  • AWS Resources — where to find the generated Terraform documentation for everything the incubator runs.

Clone this wiki locally