Skip to content

DR Machine to machine IAM scoping

Alex English edited this page Sep 13, 2026 · 4 revisions

This is a record in the Decision Records on Solutions Adopted.

Issue

Problem Statement

  • We are about to use the project tag as an authorization key — machine roles in the incubator AWS account (035866691871) will be granted access to ECR, ECS, SSM, S3 and Cognito resources on the basis of that tag matching. No definition existed of what the tag's value should be, which resources must carry it, or how the value reaches a policy.
  • This matters more than an ordinary naming convention because tag-based access control fails closed. A resource whose tag is missing or wrong is not over-shared; it becomes unreachable. A wrong tag is an outage, not a leak.
  • The live state on 2026-09-01 showed the gap is real, not theoretical:
    • Five of eight ECR repositories carried their repository name as the tag value (vrms-backend) where the project is vrms, so a vrms role would have matched neither of its own repositories.
    • 0 of 37 SSM parameters carried the tag at all, despite the secret Terraform module setting one. Struck 2026-09-09: this was wrong. All 36 parameters carried the tag, and had since it was added to the secret module in hackforla/incubator#140. The figure came from sweeping with aws ssm list-tags-for-resource in Git Bash, which rewrites any argument starting with / into a Windows path, so every call failed with InvalidResourceId and read as an untagged parameter. Corrected while auditing coverage in hackforla/incubator#197; the surviving gaps are recorded there.
    • None of the ten ECS services, none of the three Cognito user pools, and none of the 18 IAM roles carried it.

Potential Solution

1. Tag key and value. The key is project. The value is the Hack for LA project name — vrms, home-unite-us, people-depot, civic-tech-jobs, civic-tech-index. It is never an application name, an environment name, or a repository name. vrms-backend is a wrong value; vrms is the right one for both the vrms-backend and vrms-frontend repositories.

  • Changed 2026-09-13 (@ale210): the Civic Tech Index value is civic-tech-index. The other four values are hyphenated, so civictechindex was the odd one out. It is also the value that made a name-length problem visible: under the target group naming scheme in force when Epic: Make container resource naming consistent and safe to change #229 was written, civic-tech-index-backend-stage-tg was 33 characters against a hard 32-character AWS limit. That is why the spelling and the container naming scheme are being fixed as one piece of work. The length constraint itself is now historical — #224 made the container module derive a target group name that always fits — but the reason the two changes travel together is not.
  • civictechindex survives in three places that are not the project tag and must not be "corrected". The domain civictechindex.org, the S3 bucket of the same name, and the ECR repositories civictechindex-backend-prod and civictechindex-backend-stage until #225 migrates them.

2. Shared resources carry project: shared. Reversed 2026-09-12 — shared resources carry no project tag at all. Infrastructure that genuinely belongs to no single project — the ECS cluster, the load balancer, the VPC, the CloudTrail log buckets, the Terraform state buckets, ecs-ec2-role — is simply left untagged. There is no shared value.

  • The original decision, adopted 2026-09-01, was that these carry project: shared, on the reasoning that it makes "untagged" unambiguous: it would mean nobody has classified this resource yet rather than either that or it is shared. That reasoning is superseded (@ale210, 2026-09-12) and should not be re-derived. The ambiguity it warned about is real and is accepted — an untagged resource is now either shared or unclassified, and the tag alone does not distinguish them.
  • This does not affect how any policy is written. Because of decision 5, policies interpolate a project's value literally and never match on shared, so no existing or planned policy referenced the retired value.

3. Some resources cannot be tagged at all, and that is a third category. AWS-managed resources such as the KMS key alias/aws/ssm accept no tags from us. They carry no project value and could not carry one if we wanted them to; they are untaggable, and any audit or coverage report must have a bucket for them rather than reporting them as gaps forever. Since decision 2 was reversed, untaggable and shared both present as an absent tag, so an audit has to separate them by resource type rather than by reading a value.

4. Resource types that must carry the tag — when the resource belongs to a project. ECR repositories, ECS services and task definitions, SSM parameters, S3 buckets, CloudWatch log groups, Cognito user pools, and the three machine IAM roles (CI/CD, ECS execution, ECS task). Since decision 2's reversal, this is a list of the types where a project-owned resource must be tagged; an instance of one of these types that belongs to no single project is correctly untagged. The shared database's two log groups are the worked example: they are of a listed type and are deliberately untagged.

5. How the project value reaches a policy: literal interpolation, not principal tags. Policies name the project literally, interpolated by Terraform:

"Condition": { "StringEquals": { "aws:ResourceTag/project": "vrms" } }

They do not self-reference the role's own tag with ${aws:PrincipalTag/project}. Three reasons:

  • The scale argument for principal tags is that one policy can serve many principals. It does not apply here, because incubator already generates a set of roles and policies per project, so Terraform interpolation does the same job with nothing extra.
  • It removes a dependency on principal-tag propagation, which was an untested assumption: whether a role's own tags surface as aws:PrincipalTag when ECS assumes the task and execution roles.
  • It is more auditable. Reading a project's policy states exactly what that project can do, with no mental substitution.

6. The project tag on IAM roles is for inventory, not authorization. Because of decision 5, a missing tag on a role is a hygiene defect — it degrades inventory, cost allocation and the Terraform coverage report — but it does not break access. A missing tag on a resource does break access.

7. Exceptions, which are real and must not be rediscovered during implementation.

  • ecr:GetAuthorizationToken cannot be scoped. It is an account-level action supporting no resource-level permissions or conditions. It stays at Resource: "*", and the policy should carry a comment saying why.
  • S3 requires an explicit per-bucket opt-in. Attribute-based access control for general purpose buckets is disabled by default and must be enabled per bucket with PutBucketAbac (aws s3api put-bucket-abac --bucket <name> --abac-status Status=Enabled). Until it is enabled, aws:ResourceTag and s3:BucketTag conditions are not evaluated at all for that bucket, so a policy will appear to do nothing rather than fail. All five buckets in the account were Disabled on 2026-09-01.
  • Enabling S3 ABAC changes how that bucket's tags are managed. PutBucketTagging and DeleteBucketTagging stop working and are replaced by TagResource / UntagResource. This must be checked against the Terraform AWS provider before enabling it on any bucket Terraform manages, or Terraform loses the ability to manage that bucket's tags.
  • SSM SecureString values are encrypted under an account-wide key that cannot be tagged per project. All parameters use alias/aws/ssm. The kms:Decrypt grant is therefore scoped by encryption context rather than by tag: SSM passes the parameter's full ARN as PARAMETER_ARN encryption context and Parameter Store verifies it, so a role's identity policy can condition on kms:EncryptionContext:PARAMETER_ARN matching the project's parameter path, plus kms:ViaService of ssm.<region>.amazonaws.com.
    • An account-wide kms:Decrypt would not have been a leak in any case, and this is recorded so it is not re-litigated: kms:Decrypt takes a ciphertext blob as input, not a parameter name, and the only way to obtain a SecureString's ciphertext is an ssm:GetParameter call that is itself scoped. The KMS grant sits behind the SSM authorization rather than beside it. Per-project customer-managed keys were considered and are not needed.

8. Known interaction, not yet resolved: the shared ECS cluster. Services are tagged per project but the cluster incubator-prod belongs to no single project and therefore carries no project tag. ecs:UpdateService acts on the service while taking the cluster as a parameter, and IAM exposes an ecs:cluster condition key. A project's CI/CD role may therefore need an explicit allowance for the shared cluster alongside its own service. This is flagged for the policy issue that implements CI/CD scoping rather than answered here.

Feasibility Determination

  • Adopted 2026-09-01. Agreed with @ale210 during the scoping of #206. There is no separate approval comment to cite: the decisions were made in the course of building the epic, and this page is the record of them.
  • No separate feasibility analysis was performed. Instead each element was verified against the live account on 2026-09-01 before being written down:
    • Tag coverage was read directly for ECR, ECS, SSM, S3, Cognito and IAM (the figures in the Problem Statement).
    • S3 attribute-based access control was confirmed available, confirmed to cover object-level actions authorized by the bucket's tag, and confirmed Disabled on all five buckets.
    • Cognito user pools were confirmed to support constraining IAM permissions by tag.
    • The SSM encryption context PARAMETER_ARN and the account-wide alias/aws/ssm key were confirmed against the live parameters.
  • One element is a reversal of an earlier position taken during the same scoping work: the first draft proposed ${aws:PrincipalTag/project}, and decision 5 replaced it. The earlier form is not a fallback and should not be reintroduced.
  • A second reversal, later and against this record rather than against a draft: decision 2 was reversed on 2026-09-12 (@ale210). The shared tag value is retired and shared resources carry no project tag. The original wording and its reasoning are kept struck through in decision 2 so the change is visible rather than silent.
  • A third reversal, and the second against this record: decision 1's value for Civic Tech Index was changed on 2026-09-13 (@ale210), from civictechindex to civic-tech-index. Raised while scoping Epic: Make container resource naming consistent and safe to change #229, which this change gates. Unlike decision 2, the superseded value is not kept struck through in the list itself; the change is recorded in the note under decision 1.

Summary

  • Adopted, and implementation is tracked rather than complete. This record defines the standard; the work to make the account conform to it is the sub-issues of #206.
  • Directly implementing this record: #195 corrects the ECR tag values, #196 tags ECS services and task definitions, #197 audits coverage everywhere else, and #198 and #199 tag the machine roles for inventory.
  • Consuming it: #202, #203 and #204 write the scoped policies, and #205 documents how a project team requests a new permission.
  • The sequencing constraint is the important part of this record. Because tag-based access control fails closed, every tagging issue above must land and be verified before the policy issues that condition on those tags.

Clone this wiki locally