Skip to content

fix(security): narrow IAM Resource="*" on ECS/ECR/CloudFront/KMS/SES#103

Merged
cristim merged 5 commits into
feat/multicloud-web-frontendfrom
fix/iam-resource-wildcards
Apr 29, 2026
Merged

fix(security): narrow IAM Resource="*" on ECS/ECR/CloudFront/KMS/SES#103
cristim merged 5 commits into
feat/multicloud-web-frontendfrom
fix/iam-resource-wildcards

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented Apr 27, 2026

Summary

Closes 5 HIGH findings from the security review (H4–H8). Each Resource = "*" on the deploy SA / runtime Lambda role now uses the tightest scope AWS IAM allows for the action, with a Project=CUDly tag condition where the API doesn't accept a resource ARN at create time.

Finding Service Approach
H4 ECS Split into resource-scoped (cluster/cudly-*, service/cudly-*/*, task-definition/cudly-*:*), account-wide read (RegisterTaskDefinition + tags), and task-def-scoped Describe/Deregister.
H5 SES SendEmail Resource stays * (SES walks identity hierarchy — see existing comment), but new ses:FromAddress StringLike condition restricts send to *@${var.email_from_domain}. Read-only actions split into their own statement.
H6 ECR Repository ARNs scoped to arn:aws:ecr:*:*:repository/cudly-*. GetAuthorizationToken split (account-wide token endpoint, no resource ARN).
H7 CloudFront Create+Read stay * (ARN unknown at create), but mutate (Update/Delete/Tag) requires aws:ResourceTag/Project = "CUDly".
H8 KMS Same pattern as CloudFront: read/create stay broad, but mutate (ScheduleKeyDeletion, DisableKey, PutKeyPolicy, etc.) requires aws:ResourceTag/Project = "CUDly".

⚠ Operator action required

terraform/environments/aws/ci-cd-permissions/ is a bootstrap module per the CLAUDE.md CI/CD IAM split — applied manually by a privileged human, not by the CI workflow itself. After merging this PR, re-apply that directory out of band:

cd terraform/environments/aws/ci-cd-permissions
terraform plan
terraform apply

Until that re-apply happens, the deploy SA still has the old (broader) grants and nothing breaks. After re-apply, any drift detection on third-party resources sharing the same account that the deploy SA happens to have been touching becomes visible — that's the fix taking effect.

Test plan

  • terraform fmt -check clean.
  • terraform validate green for environments/aws/ci-cd-permissions/.
  • No runtime code paths touched — Go test suite unaffected.
  • Operator (post-merge): terraform plan against the bootstrap module in a sandbox account; confirm no live CUDly resources hit "access denied"; then re-apply in prod.
  • Operator (post-merge): smoke-test a CUDly redeploy (Lambda push, ECR push, CloudFront update) to confirm the deploy SA still works for legitimate operations.

Follow-up PR

PR5 (supply chain) — independent of this PR.

🤖 Generated with claude-flow

Summary by CodeRabbit

  • Chores
    • Tightened ECR, CloudFront, ECS and KMS permissions into resource- and tag-scoped statements; destructive/mutation actions now require the project tag.
    • Restricted SES send permissions to the project email domain and added read-only SES access for account/identity info; removed broad send allowance.
    • Added and attached a supplemental managed IAM policy to split large permission sets and enable finer-grained mutation scopes.

Closes 5 HIGH findings (H4–H8) from the security review. Each Resource="*"
on the deploy SA / runtime Lambda role now uses the tightest scope AWS IAM
allows for the action, falling back to a Project=CUDly tag condition where
the API doesn't accept a resource ARN at create time.

H4 — ECS (policy_compute.tf):
  Split the previous all-* statement into three:
    1. Resource-scoped: Create/Update/Delete on cluster/cudly-*,
       service/cudly-*/*, task/cudly-*/*, task-definition/cudly-*:*.
    2. Account-wide read: RegisterTaskDefinition (no resource ARN at
       registration time) + ListTagsForResource (read-only).
    3. Task-definition-scoped: Deregister/Describe limited to
       task-definition/cudly-*:*.
  The deploy SA can no longer touch unrelated ECS clusters/services in
  the same account.

H5 — SES SendEmail (modules/compute/aws/lambda/main.tf):
  Added `ses:FromAddress` StringLike condition restricting send actions
  to *@${var.email_from_domain}. The Resource stays "*" because SES walks
  the identity hierarchy (see existing comment), but a compromised Lambda
  can no longer spoof from arbitrary verified identities — only from
  CUDly's configured domain. Read-only ses:GetAccount/GetEmailIdentity
  split into a separate statement (no spoofing risk).

H6 — ECR (policy_compute.tf):
  Repository-scoped actions now require resource ARN
  arn:aws:ecr:*:*:repository/cudly-*. ecr:GetAuthorizationToken split
  into its own statement because it's an account-wide token endpoint
  with no resource ARN.

H7 — CloudFront (policy_compute.tf):
  Split into Create+Read (Resource=*) and Mutate (Resource=* +
  StringEquals aws:ResourceTag/Project=CUDly). The Terraform
  aws_cloudfront_distribution sets the Project tag at creation time, so
  CUDly distributions remain manageable while attempts to update or
  delete unrelated distributions in the same account are denied.

H8 — KMS (policy_compute.tf):
  Same pattern as CloudFront: read/create stay broad (key ARN unknown
  at create time, terraform plan needs to enumerate), but mutating
  actions (ScheduleKeyDeletion, DisableKey, PutKeyPolicy, etc.) now
  require aws:ResourceTag/Project=CUDly. Closes the path where the
  deploy SA could schedule deletion of unrelated KMS keys.

Operator action required: this is a bootstrap module per CLAUDE.md's
CI/CD IAM split — `terraform/environments/aws/ci-cd-permissions/` is
applied manually by a privileged human, not by the CI workflow itself.
After merging, re-apply that directory out of band. Until then, the
deploy SA still has the old (broader) grants and nothing breaks.

Validation:
- terraform fmt clean
- terraform validate green
- No runtime code paths touched
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 27, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 016468bb-1beb-4910-b918-c898c711efba

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Split and narrowed CI/CD compute IAM policies: ECR, CloudFront, ECS, SES, and KMS permissions were re-scoped into ARN- and tag-restricted statements; a new managed policy was added and attached to the deploy role; Lambda SES send permissions were constrained to the project email domain and SES read actions were added.

Changes

Cohort / File(s) Summary
Primary compute IAM policy
terraform/environments/aws/ci-cd-permissions/policy_compute.tf
Reworked IAM policy: ECR actions split into repository-ARN-scoped permissions plus account-wide ecr:GetAuthorizationToken; CloudFront create/read vs mutation split with mutation gated by aws:ResourceTag/Project = "CUDly" (CloudFront function mutation removed from this file); ECS permissions reorganized into ARN-scoped cluster/service/task/task-definition statements and separate task-definition registration/listing/deregistration statements; SES SendEmail removed; KMS create/read separated from tag-gated mutation/destructive actions.
Supplemental compute policy
terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf
Added aws_iam_policy.compute_b containing ARN-scoped allows: CloudFront Function mutation scoped to arn:aws:cloudfront::*:function/cudly-*; ecs:ListTasks allowed with ArnLike condition on ecs:cluster for cudly-*; KMS alias mutation scoped to arn:aws:kms:*:*:alias/cudly-*. Policy tagged with Project="CUDly", ManagedBy="terraform".
Role attachment
terraform/environments/aws/ci-cd-permissions/role.tf
Added aws_iam_role_policy_attachment.compute_b to attach the new compute_b managed policy to the cudly_deploy role.
Lambda SES policy changes
terraform/modules/compute/aws/lambda/main.tf
Constrained ses:SendEmail and ses:SendRawEmail with ses:FromAddress StringLike condition *@${var.email_from_domain} (replaced previous broader send statement); added a read-only SES statement (ses:GetAccount, ses:GetEmailIdentity); kept configuration-set access scoped to arn:aws:ses:*:*:configuration-set/${var.stack_name}*.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

priority/p0, severity/critical, urgency/now, impact/few, type/bug

Poem

🐇 I hopped through policies, trimmed each thread,
Scoped ARNs and tags where permissions tread.
I nudged sends to one domain, tidy and bright,
Split a policy, then clipped permissions tight.
Hooray — a rabbit dances in the cloud tonight! 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main security-focused change: narrowing overly broad IAM Resource="*" statements across five services (ECS, ECR, CloudFront, KMS, SES).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/iam-resource-wildcards

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/all-users Affects every user effort/m Days type/security Security finding labels Apr 28, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 28, 2026

P1 — closes H4–H8: narrows 5 IAM Resource="*" statements on the deploy SA/runtime Lambda role using ARN prefixes (cudly-*) and tag conditions (Project=CUDly). Requires a manual terraform apply of terraform/environments/aws/ci-cd-permissions/ post-merge (bootstrap module — not applied by CI). CI green, MERGEABLE. (triage agent wave2-E)

@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 28, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
terraform/environments/aws/ci-cd-permissions/policy_compute.tf (1)

280-318: ⚠️ Potential issue | 🟠 Major

Split KMS alias actions out from the key-tagged statement.

AWS KMS requires permissions on both the alias and the associated key for kms:DeleteAlias and kms:UpdateAlias operations. The condition key aws:ResourceTag/Project applies only to key resources, not alias resources. With kms:CreateAlias unrestricted in the broad bucket and kms:DeleteAlias/kms:UpdateAlias gated only by key tags, alias management is both broader than intended and inconsistently scoped. A separate statement targeting alias ARNs for alias operations would properly gate alias management independently from key resource tagging. (docs.aws.amazon.com/kms)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terraform/environments/aws/ci-cd-permissions/policy_compute.tf` around lines
280 - 318, The KMS alias actions are mixed into the "KMSMutateTaggedOnly"
statement which uses a key-only condition ("aws:ResourceTag/Project") and thus
does not correctly constrain alias operations; split alias-specific actions into
their own statement: remove "kms:DeleteAlias" and "kms:UpdateAlias" from the Sid
"KMSMutateTaggedOnly" Action list and add a new statement (e.g., Sid
"KMSAliasMutate") that grants "kms:CreateAlias", "kms:DeleteAlias", and
"kms:UpdateAlias" (and any other alias-only actions) with Resource set to alias
ARNs (arn:aws:kms:*:*:alias/*) and appropriate conditions (aliases cannot be
conditioned by aws:ResourceTag/Project), so alias permission scope is explicit
and separate from key-tag gated key mutations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@terraform/environments/aws/ci-cd-permissions/policy_compute.tf`:
- Around line 137-153: The CloudFrontMutateTaggedOnly statement mixes
distribution mutations with function actions that do not support the
aws:ResourceTag condition; remove "cloudfront:DeleteFunction",
"cloudfront:PublishFunction", and "cloudfront:UpdateFunction" from the
CloudFrontMutateTaggedOnly statement and keep the StringEquals
"aws:ResourceTag/Project" = "CUDly" condition only for distribution-related
actions (e.g., DeleteDistribution, UpdateDistribution) so tag scoping applies;
then add a new separate statement granting the function actions and scope them
to explicit CloudFront Function ARNs (use the
arn:aws:cloudfront::ACCOUNT:function/function-name pattern or a narrower
ARN/wildcard) with no aws:ResourceTag condition; reference the existing
statement name CloudFrontMutateTaggedOnly for removal and create a new statement
(e.g., CloudFrontFunctionMutateScoped) for the function actions.
- Around line 172-194: The policy block with Sid "ECSFargateResourceScoped"
improperly scopes the action ecs:ListTasks to specific ARNs; create a new IAM
statement that contains only Action = ["ecs:ListTasks"] with Resource = ["*"]
(keeping the existing ECSFargateResourceScoped statement for the other actions),
and if you need to limit it further add an appropriate Condition using the
ecs:cluster key to restrict to clusters like "arn:aws:ecs:*:*:cluster/cudly-*".

---

Outside diff comments:
In `@terraform/environments/aws/ci-cd-permissions/policy_compute.tf`:
- Around line 280-318: The KMS alias actions are mixed into the
"KMSMutateTaggedOnly" statement which uses a key-only condition
("aws:ResourceTag/Project") and thus does not correctly constrain alias
operations; split alias-specific actions into their own statement: remove
"kms:DeleteAlias" and "kms:UpdateAlias" from the Sid "KMSMutateTaggedOnly"
Action list and add a new statement (e.g., Sid "KMSAliasMutate") that grants
"kms:CreateAlias", "kms:DeleteAlias", and "kms:UpdateAlias" (and any other
alias-only actions) with Resource set to alias ARNs (arn:aws:kms:*:*:alias/*)
and appropriate conditions (aliases cannot be conditioned by
aws:ResourceTag/Project), so alias permission scope is explicit and separate
from key-tag gated key mutations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f359648f-5659-4a4f-9541-ebf889bcdc61

📥 Commits

Reviewing files that changed from the base of the PR and between e9ab8ba and 1957279.

📒 Files selected for processing (2)
  • terraform/environments/aws/ci-cd-permissions/policy_compute.tf
  • terraform/modules/compute/aws/lambda/main.tf

Comment on lines +137 to +153
Sid = "CloudFrontMutateTaggedOnly"
Effect = "Allow"
Action = [
"cloudfront:DeleteDistribution",
"cloudfront:DeleteFunction",
"cloudfront:PublishFunction",
"cloudfront:TagResource",
"cloudfront:UntagResource",
"cloudfront:UpdateDistribution",
"cloudfront:UpdateFunction",
]
Resource = "*"
Condition = {
StringEquals = {
"aws:ResourceTag/Project" = "CUDly"
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

AWS CloudFront service authorization reference function resource type ResourceTag

💡 Result:

In AWS CloudFront's Service Authorization Reference, ResourceTag refers to the resource condition key #amazoncloudfront:aws:ResourceTag/${TagKey}, available for all taggable CloudFront resource types. This key allows attribute-based access control (ABAC) in IAM policies by evaluating tags on CloudFront resources like distributions, streaming-distributions, anycast-ip-list, vpcorigin, distribution-tenant, connection-group, trust-store, and connection-function. Example ARN format for distribution: arn:${Partition}:cloudfront::${Account}:distribution/${DistributionId} Condition keys column lists #amazoncloudfront:aws:ResourceTag/${TagKey} for each, enabling policies like: { "Condition": { "StringEquals": { "cloudfront:aws:ResourceTag/environment": "production" } } } This supports tag-based permissions on supported resources. CloudFront partially supports ABAC for taggable resources. Tagging is managed via console, API, CLI for distributions and other listed types (not origin access identities or invalidations).

Citations:


🌐 Web query:

CloudFront DeleteFunction PublishFunction UpdateFunction IAM resource types

💡 Result:

The IAM actions cloudfront:DeleteFunction, cloudfront:PublishFunction, and cloudfront:UpdateFunction all use the CloudFront function resource type. Resource type: function ARN format: arn:${Partition}:cloudfront::${Account}:function/${Name} These actions support resource-level permissions, so specify the function ARN in the IAM policy Resource element. No condition keys are defined for the function resource. Example IAM policy statement: { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "cloudfront:DeleteFunction", "cloudfront:PublishFunction", "cloudfront:UpdateFunction" ], "Resource": "arn:aws:cloudfront::123456789012:function/my-function" } ] }

Citations:


🌐 Web query:

CloudFront function resource type tagging support aws:ResourceTag condition

💡 Result:

CloudFront Functions (resource type AWS::CloudFront::Function, ARN arn:aws:cloudfront::account:function/name) do not support aws:ResourceTag condition keys in IAM policies. The official AWS Service Authorization Reference lists no condition keys (empty column) for this resource type, unlike distributions and other CloudFront resources which explicitly support #aws:ResourceTag/${TagKey}. CloudFront provides general TagResource/TagFunction APIs that accept function ARNs, indicating functions can be tagged. However, the IAM service authorization documentation confirms no support for tag-based conditions (aws:ResourceTag) specifically for CloudFront Functions. This means you cannot use conditions like "StringEquals": {"aws:ResourceTag/MyTag": "MyValue"} to restrict CloudFront function actions (e.g., cloudfront:CreateFunction, cloudfront:PublishFunction, cloudfront:UpdateFunction) based on resource tags.

Citations:


Split function permissions from distribution mutations—CloudFront Functions don't support the aws:ResourceTag condition.

This statement mixes distribution mutations with cloudfront:DeleteFunction, cloudfront:PublishFunction, and cloudfront:UpdateFunction. The CloudFront function resource type does not support aws:ResourceTag as a condition key in IAM policies. This means the Project=CUDly tag condition will not apply to or restrict function actions—leaving function permissions unscoped by tags. Move function actions into a separate statement with explicit ARN-based resource scoping (e.g., arn:aws:cloudfront::ACCOUNT:function/function-name), and keep tag-based conditions only for resource types that support them (distributions, streaming distributions, etc.).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terraform/environments/aws/ci-cd-permissions/policy_compute.tf` around lines
137 - 153, The CloudFrontMutateTaggedOnly statement mixes distribution mutations
with function actions that do not support the aws:ResourceTag condition; remove
"cloudfront:DeleteFunction", "cloudfront:PublishFunction", and
"cloudfront:UpdateFunction" from the CloudFrontMutateTaggedOnly statement and
keep the StringEquals "aws:ResourceTag/Project" = "CUDly" condition only for
distribution-related actions (e.g., DeleteDistribution, UpdateDistribution) so
tag scoping applies; then add a new separate statement granting the function
actions and scope them to explicit CloudFront Function ARNs (use the
arn:aws:cloudfront::ACCOUNT:function/function-name pattern or a narrower
ARN/wildcard) with no aws:ResourceTag condition; reference the existing
statement name CloudFrontMutateTaggedOnly for removal and create a new statement
(e.g., CloudFrontFunctionMutateScoped) for the function actions.

Comment on lines +172 to +194
Sid = "ECSFargateResourceScoped"
Effect = "Allow"
Action = [
"ecs:CreateCluster",
"ecs:CreateService",
"ecs:DeleteCluster",
"ecs:DeleteService",
"ecs:DeregisterTaskDefinition",
"ecs:DescribeClusters",
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"ecs:ListTagsForResource",
"ecs:ListTasks",
"ecs:PutClusterCapacityProviders",
"ecs:StopTask",
"ecs:RegisterTaskDefinition",
"ecs:TagResource",
"ecs:UntagResource",
"ecs:UpdateCluster",
"ecs:UpdateService",
]
Resource = [
"arn:aws:ecs:*:*:cluster/cudly-*",
"arn:aws:ecs:*:*:service/cudly-*/*",
"arn:aws:ecs:*:*:task/cudly-*/*",
"arn:aws:ecs:*:*:task-definition/cudly-*:*",
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

ecs:ListTasks AWS resource type supported container-instance documentation

💡 Result:

Yes, the AWS ECS ListTasks API supports filtering by container-instance. The 'containerInstance' parameter accepts the container instance ID or full ARN and limits results to tasks belonging to that container instance. This is documented in the official AWS ECS API reference.

Citations:


🌐 Web query:

AWS service authorization reference ecs:ListTasks resource types supported

💡 Result:

The AWS ecs:ListTasks action does not support resource-level permissions. In IAM policies, you must specify "Resource": "" (all resources) to grant permission for this action. This is indicated in the official AWS Service Authorization Reference for Amazon ECS, where List actions like ecs:ListTasks have no specific resource types listed in the "Resource types" column of the Actions table. Per AWS documentation: "If there is no value for this column [Resource types], you must specify all resources ("") to which the policy applies in the Resource element of your policy statement." Examples in AWS ECS IAM policy documentation consistently use "Resource": "" for ecs:ListTasks and ecs:List actions, often combined with conditions like "ArnEquals": {"ecs:cluster": "arn:..."} to restrict to specific clusters. ECS defines resource types such as cluster, service, task, container-instance, task-definition, etc., but ecs:ListTasks (a List access level action) is not scoped to any of them.

Citations:


ecs:ListTasks does not support resource-level permissions and must use "Resource": "*".

According to AWS service authorization documentation, List actions like ecs:ListTasks cannot be scoped to specific resource types. Move ecs:ListTasks to its own statement with "Resource": "*" and optionally use the ecs:cluster condition key to restrict it to specific clusters if needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terraform/environments/aws/ci-cd-permissions/policy_compute.tf` around lines
172 - 194, The policy block with Sid "ECSFargateResourceScoped" improperly
scopes the action ecs:ListTasks to specific ARNs; create a new IAM statement
that contains only Action = ["ecs:ListTasks"] with Resource = ["*"] (keeping the
existing ECSFargateResourceScoped statement for the other actions), and if you
need to limit it further add an appropriate Condition using the ecs:cluster key
to restrict to clusters like "arn:aws:ecs:*:*:cluster/cudly-*".

CodeRabbit flagged three statements in policy_compute.tf where the
intended scoping is silently broken because the AWS service does not
support the condition key or resource type used. In all three cases
the policy LOOKED narrow but actually granted account-wide access for
the affected actions:

  1. CloudFront functions (DeleteFunction/PublishFunction/
     UpdateFunction) operate on the `function` resource type, which
     does not support the aws:ResourceTag condition key per the AWS
     Service Authorization Reference. They were grouped with
     distribution mutations under CloudFrontMutateTaggedOnly with a
     `aws:ResourceTag/Project=CUDly` condition that silently
     evaluates to no-match for functions, so the deploy SA could
     mutate any CloudFront function in the account.

  2. ecs:ListTasks does not support resource-level permissions per
     the AWS Service Authorization Reference (no resource types are
     listed for the action). It was bundled with cluster/service/task
     ARNs under ECSFargateResourceScoped where AWS silently ignores
     the Resource list, so the deploy SA could list tasks in any
     cluster in the account.

  3. KMS alias actions (CreateAlias/DeleteAlias/UpdateAlias) act on
     the `alias` resource type, but the aws:ResourceTag/Project gate
     applies only to keys (aliases inherit no IAM-visible tags).
     CreateAlias was in the unrestricted KMSCreateAndRead bucket and
     DeleteAlias/UpdateAlias in the key-tag-gated bucket, so the
     deploy SA could create or rename aliases on any KMS key in the
     account.

Each fix replaces the silently-no-op tag/ARN scoping with explicit
ARN-prefix scoping that AWS will actually enforce. Net effect tightens
permissions; no new grants are introduced.

Structural note — split into a second managed policy:

The existing `aws_iam_policy.compute` is at AWS's 6144-char default
managed-policy limit. The three new ARN-scoped statements wouldn't fit
without evicting unrelated existing grants, so they live in a new
`aws_iam_policy.compute_b` (file `policy_compute_b.tf`) attached to
the same role. Three statements moved out, three statements added in.

Verification:
  - terraform validate, terraform fmt, tflint all pass.
  - tflint's aws_iam_policy_too_long_policy rule confirms the main
    policy is back under 6144 chars after the alias/ListTasks
    deletions.
  - Each new statement uses an AWS-supported condition key or
    resource type per the Service Authorization Reference (verified
    against docs cited in CR's review).
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 28, 2026

CR triage — round 1 (commit eccfdf7ca)

CR posted 3 actionable findings, all on terraform/environments/aws/ci-cd-permissions/policy_compute.tf. All 3 are correct on AWS-doc grounds (CR did web queries against the AWS Service Authorization Reference, and I cross-checked) — the policy looked narrow in each case but the chosen condition key or resource type silently no-ops, leaving the action effectively account-wide. Fixed all 3.

# Finding Status Fix
1 CloudFrontMutateTaggedOnly mixed cloudfront:DeleteFunction/PublishFunction/UpdateFunction with distribution mutations under an aws:ResourceTag/Project=CUDly condition. The CloudFront function resource type does not support aws:ResourceTag per the AWS Service Authorization Reference, so the gate silently no-ops. Fixed Removed function actions from CloudFrontMutateTaggedOnly; added CloudFrontFnMutate with Resource = "arn:aws:cloudfront::*:function/cudly-*" (CF function ARNs are global, no region segment).
2 ECSFargateResourceScoped listed ecs:ListTasks alongside cluster/service/task ARNs. Per the AWS Service Authorization Reference, ecs:ListTasks is a List action with no resource types, so AWS silently ignores Resource and the policy admits ListTasks against any cluster in the account. Fixed Removed ecs:ListTasks from ECSFargateResourceScoped; added ECSListTasks with Resource="*" and Condition.ArnLike.ecs:cluster = "arn:aws:ecs:*:*:cluster/cudly-*" (the ecs:cluster key IS supported for ListTasks).
3 KMSCreateAndRead granted kms:CreateAlias unrestricted, and KMSMutateTaggedOnly gated kms:DeleteAlias/UpdateAlias with aws:ResourceTag/Project=CUDly — but that tag condition matches only key resources, not aliases (aliases inherit no IAM-visible tags). Result: deploy SA could create/rename aliases on any KMS key in the account. Fixed (outside-diff comment) Removed alias actions from both buckets; added KMSAliasMutate with Resource = "arn:aws:kms:*:*:alias/cudly-*".

Structural note — split into a second managed policy

aws_iam_policy.compute was at AWS's 6144-char default managed-policy limit. Adding the three new ARN-scoped statements wouldn't fit without evicting unrelated existing grants, so they live in a new aws_iam_policy.compute_b (file policy_compute_b.tf) attached to the same role via role.tf. Three statements moved out of the main policy, three statements added to the new one — net deploy-SA permissions tighten in every case, no new grants.

Verification

  • terraform validate, terraform fmt -check, and tflint (with the AWS plugin's aws_iam_policy_too_long_policy rule) all pass locally.
  • Each new statement uses an AWS-supported condition key or resource type per the Service Authorization Reference.
  • Pre-commit hooks ran cleanly.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the list of changed files in the PR
gh pr view 103 --repo LeanerCloud/CUDly --json files --jq '.files[].path'

# Get the current state of the policy files
gh api repos/LeanerCloud/CUDly/pulls/103/files --jq '.[].filename'

Length of output: 452


🏁 Script executed:

#!/bin/bash
# Get the full diff of the PR
gh api repos/LeanerCloud/CUDly/pulls/103 --jq '.head.sha'

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Fetch file contents from the PR head
for f in \
  "terraform/environments/aws/ci-cd-permissions/policy_compute.tf" \
  "terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf" \
  "terraform/environments/aws/ci-cd-permissions/role.tf" \
  "terraform/modules/compute/aws/lambda/main.tf"; do
  echo "===== $f ====="
  gh api "repos/LeanerCloud/CUDly/contents/$f?ref=eccfdf7cac1a9d23bc83345a82f2820b72f8ccc8" --jq '.content' | base64 -d
  echo ""
done

Length of output: 31701


@cristim — excellent triage write-up; all three original findings were valid and well-fixed. The incremental review of eccfdf7ca is below.


Round-2 findings on policy_compute_b.tf / policy_compute.tf

🔴 KMSAliasMutate — key-resource permission missing for all three alias operations

kms:CreateAlias, kms:DeleteAlias, and kms:UpdateAlias each require two separate IAM permission grants: one on the alias resource and one on the underlying KMS key resource.

AWS documents this clearly: to create an alias, the principal needs permissions for both the alias and for the associated KMS key — kms:CreateAlias on the alias (IAM policy) and kms:CreateAlias on the KMS key (key policy or IAM policy delegated from the key policy).

This exact gap has burned teams before — a policy granting kms:CreateAlias only on the key ARN produced an AccessDeniedException referencing the alias ARN; adding the alias ARN as a second resource fixed it, confirming both resources are independently evaluated.

The current KMSAliasMutate statement only covers the alias side:

{
  Sid    = "KMSAliasMutate"
  Effect = "Allow"
  Action = ["kms:CreateAlias", "kms:DeleteAlias", "kms:UpdateAlias"]
  Resource = "arn:aws:kms:*:*:alias/cudly-*"   # ← alias side only
  # missing: "arn:aws:kms:*:*:key/*" for the key side
}

Without an explicit kms:CreateAlias / kms:DeleteAlias / kms:UpdateAlias on a key ARN in the IAM policy, the call succeeds only if the KMS key policy itself explicitly grants these actions to the deploy SA role — which is fragile and depends on how modules/security/aws/kms_signing.tf constructs the key policy. As AWS notes, "principals who manage aliases must have permission to call the alias operation on the alias and on all affected KMS keys."

Fix: add "arn:aws:kms:*:*:key/*" to the KMSAliasMutate resource list. This is not a regression — the alias ARN scope already prevents the SA from creating/renaming aliases outside the cudly-* namespace, and any key can host a cudly-* alias legitimately:

{
  Sid    = "KMSAliasMutate"
  Effect = "Allow"
  Action = [
    "kms:CreateAlias",
    "kms:DeleteAlias",
    "kms:UpdateAlias",
  ]
  Resource = [
    "arn:aws:kms:*:*:alias/cudly-*",
    "arn:aws:kms:*:*:key/*",   # required by CreateAlias/DeleteAlias/UpdateAlias on key side
  ]
}

Note: kms:UpdateAlias re-points an alias from one key to another, so it needs the key ARN for both source and destination keys — a wildcard key/* covers both without having to enumerate them.


🟡 policy_compute.tf SES — deploy SA retains ses:SendEmail without condition

The SES statement in the deploy SA policy (aws_iam_policy.compute) still includes ses:SendEmail with Resource = "*" and no ses:FromAddress condition:

{
  Sid    = "SES"
  Effect = "Allow"
  Action = [
    ...
    "ses:SendEmail",   # ← no condition, Resource = "*"
    ...
  ]
  Resource = "*"
}

The deploy SA is a Terraform CI/CD service account — it creates/verifies SES identities, configures DKIM, and manages tags, but has no operational need to send email. A compromised CI pipeline could use this to send email from any verified identity in the account with no domain restriction, whereas the Lambda runtime is at least gated by ses:FromAddress = *@${var.email_from_domain}. Either remove ses:SendEmail from the deploy SA policy, or at minimum add the same ses:FromAddress condition the Lambda uses.


✅ Round-1 fixes confirmed correct

Statement Verdict
CloudFrontFnMutate — ARN-scoped to arn:aws:cloudfront::*:function/cudly-* ✅ Correct. CF function resource type does not support aws:ResourceTag; ARN scope is the right gate.
ECSListTasksResource="*" + ArnLike ecs:cluster condition ✅ Correct. ecs:ListTasks has no resource-level permissions; ecs:cluster condition key is a supported context key for ListTasks per the AWS SAR.
KMSAliasMutate (alias ARN scope, removed from tag-gated bucket) ✅ Conceptually correct split; see the blocking issue above about the missing key ARN.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf`:
- Around line 53-60: The KMSAliasMutate statement currently only allows alias
ARNs which is insufficient for kms:CreateAlias and kms:UpdateAlias because AWS
evaluates both the alias resource and the target key resource; update the
statement with Sid "KMSAliasMutate" to grant the same actions
("kms:CreateAlias","kms:DeleteAlias","kms:UpdateAlias") on both the alias ARNs
and the corresponding key ARNs (e.g. include "arn:aws:kms:*:*:key/*" or the
specific key ARNs) so Create/Update flows have permission on the target key as
well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f879d72-668f-46fa-a827-28e5529d5725

📥 Commits

Reviewing files that changed from the base of the PR and between 1957279 and eccfdf7.

📒 Files selected for processing (3)
  • terraform/environments/aws/ci-cd-permissions/policy_compute.tf
  • terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf
  • terraform/environments/aws/ci-cd-permissions/role.tf
🚧 Files skipped from review as they are similar to previous changes (1)
  • terraform/environments/aws/ci-cd-permissions/policy_compute.tf

Comment on lines +53 to +60
Sid = "KMSAliasMutate"
Effect = "Allow"
Action = [
"kms:CreateAlias",
"kms:DeleteAlias",
"kms:UpdateAlias",
]
Resource = "arn:aws:kms:*:*:alias/cudly-*"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In AWS IAM Service Authorization Reference for KMS, what resource types are required for kms:CreateAlias, kms:UpdateAlias, and kms:DeleteAlias? Confirm whether CreateAlias/UpdateAlias require both alias and key resources.

💡 Result:

In the AWS IAM Service Authorization Reference for KMS, the resource types for kms:CreateAlias, kms:UpdateAlias, and kms:DeleteAlias are both "alias" and "key". KMS defines two resource types: alias (arn:aws:kms:::alias/) and key (arn:aws:kms:::key/). These actions require permissions on both resource types: - kms:CreateAlias requires kms:CreateAlias permission on the alias (IAM policy) and on the target key (key policy). - kms:DeleteAlias requires kms:DeleteAlias permission on the alias (IAM policy) and on the associated key (key policy). - kms:UpdateAlias requires kms:UpdateAlias permission on the alias (IAM policy), on the current key (key policy), and on the new target key (key policy). Yes, CreateAlias and UpdateAlias require both alias and key resources (permissions on both types). DeleteAlias also requires both.

Citations:


🏁 Script executed:

# First, find the file and check if it exists
fd -t f "policy_compute_b.tf" -x wc -l {}

# Then read the specific lines mentioned
cat -n terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf | sed -n '50,65p'

Repository: LeanerCloud/CUDly

Length of output: 528


🏁 Script executed:

# Search for other KMS policies in the same directory
grep -r "kms:CreateAlias\|kms:UpdateAlias\|kms:DeleteAlias" terraform/environments/aws/ci-cd-permissions/ -A 5 -B 5

# Also check if there are other policy files that might have similar issues
fd -t f "policy_.*\.tf" terraform/environments/aws/ci-cd-permissions/

Repository: LeanerCloud/CUDly

Length of output: 3871


KMS alias mutations are under-authorized for Create/Update flows.

Line 60 scopes only alias ARNs. For kms:CreateAlias and kms:UpdateAlias, AWS KMS authorization evaluates both the alias resource and the target key resource. Without key permissions, these operations will fail at deploy time with AccessDenied.

🔧 Proposed fix
       {
         Sid    = "KMSAliasMutate"
         Effect = "Allow"
         Action = [
           "kms:CreateAlias",
           "kms:DeleteAlias",
           "kms:UpdateAlias",
         ]
-        Resource = "arn:aws:kms:*:*:alias/cudly-*"
+        Resource = [
+          "arn:aws:kms:*:*:alias/cudly-*",
+          "arn:aws:kms:*:*:key/*",
+        ]
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Sid = "KMSAliasMutate"
Effect = "Allow"
Action = [
"kms:CreateAlias",
"kms:DeleteAlias",
"kms:UpdateAlias",
]
Resource = "arn:aws:kms:*:*:alias/cudly-*"
Sid = "KMSAliasMutate"
Effect = "Allow"
Action = [
"kms:CreateAlias",
"kms:DeleteAlias",
"kms:UpdateAlias",
]
Resource = [
"arn:aws:kms:*:*:alias/cudly-*",
"arn:aws:kms:*:*:key/*",
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf` around
lines 53 - 60, The KMSAliasMutate statement currently only allows alias ARNs
which is insufficient for kms:CreateAlias and kms:UpdateAlias because AWS
evaluates both the alias resource and the target key resource; update the
statement with Sid "KMSAliasMutate" to grant the same actions
("kms:CreateAlias","kms:DeleteAlias","kms:UpdateAlias") on both the alias ARNs
and the corresponding key ARNs (e.g. include "arn:aws:kms:*:*:key/*" or the
specific key ARNs) so Create/Update flows have permission on the target key as
well.

…eck (CR r2)

CodeRabbit flagged that KMSAliasMutate in policy_compute_b.tf only
allows alias ARNs, which is insufficient: AWS KMS evaluates
kms:CreateAlias / kms:DeleteAlias / kms:UpdateAlias against BOTH the
alias resource AND the target key resource. Without permissions on
the target key, every CreateAlias/UpdateAlias call (and DeleteAlias)
would 403 at deploy time.

The CR-proposed fix added "arn:aws:kms:*:*:key/*" to the alias
statement's Resource list with no condition, which would allow alias
mutations against ANY key in the account. Tighter alternative: add
the three alias actions to the existing KMSMutateTaggedOnly statement
in policy_compute.tf, which is already tag-gated to keys carrying
Project=CUDly. The deploy SA can now operate aliases targeting the
CUDly OIDC signing key (and any future CUDly keys) but not other
keys in the same account.

The dual-check pattern is now:
  - Alias side: policy_compute_b.tf KMSAliasMutate, ARN-scoped to
    arn:aws:kms:*:*:alias/cudly-*.
  - Key side: policy_compute.tf KMSMutateTaggedOnly, Resource="*"
    with aws:ResourceTag/Project=CUDly.

Both must allow for the API call to succeed, which is exactly the
authorization model AWS uses for these actions per the AWS KMS
authorization docs.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 28, 2026

CR triage — round 2 (commit 57fefbcfa)

CR posted 1 actionable finding on policy_compute_b.tf line 60: KMSAliasMutate only granted alias ARN permissions, but AWS KMS evaluates CreateAlias/UpdateAlias/DeleteAlias against BOTH the alias resource AND the target key resource. Without key permissions, deploys would 403 at apply time. CR cited the AWS Service Authorization Reference and the KMS alias-access docs; both confirm the dual-resource evaluation. Verified — fixed.

# Finding Status Fix
1 KMSAliasMutate scoped only to alias ARNs; missing the key-side check that KMS requires for CreateAlias/UpdateAlias/DeleteAlias. Fixed Added kms:CreateAlias, kms:DeleteAlias, kms:UpdateAlias back to KMSMutateTaggedOnly in policy_compute.tf (existing tag-gated key statement). The alias-side check stays in policy_compute_b.tf KMSAliasMutate (ARN-scoped). Both must allow for the API call to succeed, matching AWS's dual-resource authorization model.

Note: tightened beyond CR's proposed fix

CR's suggested patch added arn:aws:kms:*:*:key/* to the alias statement's Resource list with no condition — that would have allowed alias mutations against ANY key in the account. Instead, the key-side check piggybacks on the existing KMSMutateTaggedOnly statement, which is already tag-gated to Project=CUDly. So the deploy SA can mutate aliases pointing at CUDly-owned keys (the OIDC signing key, etc.) but not at unrelated keys in the same account.

Verification: terraform fmt, terraform validate, and tflint (incl. AWS-plugin policy-too-long check) all pass. Pre-commit hooks ran cleanly.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest commit SHA on the PR branch
gh api repos/LeanerCloud/CUDly/pulls/103 --jq '.head.sha'

Length of output: 41


🏁 Script executed:

#!/bin/bash
SHA="57fefbcfa80d984fbe82f5ac0a4b744db7151cfa"
for f in \
  "terraform/environments/aws/ci-cd-permissions/policy_compute.tf" \
  "terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf"; do
  echo "===== $f ====="
  gh api "repos/LeanerCloud/CUDly/contents/$f?ref=$SHA" --jq '.content' | base64 -d
  echo ""
done

Length of output: 14840


@cristim — I've read the current state of both files at 57fefbcfa. Here's my assessment:


Round-2 triage — verified ✅

KMS dual-resource fix — confirmed correct and tighter than suggested

The implementation is exactly right. KMS's dual-resource evaluation is now satisfied by two independent statements:

Side Statement Scope
Alias-side KMSAliasMutate (policy_compute_b.tf) arn:aws:kms:*:*:alias/cudly-*
Key-side KMSMutateTaggedOnly (policy_compute.tf) Resource="*" + aws:ResourceTag/Project = "CUDly"

Both Allow grants must be present for KMS to authorize the call — the alias must be cudly-* and the target key must be tagged Project=CUDly. This is meaningfully tighter than my proposed fix (arn:aws:kms:*:*:key/* with no condition), which would have permitted alias mutations against any key in the account. Your approach correctly limits the key side to CUDly-owned keys only. ✅


🟡 Still-open: deploy SA ses:SendEmail without condition

The SES statement in policy_compute.tf was not addressed in the round-2 triage. The current state is:

{
  Sid    = "SES"
  Effect = "Allow"
  Action = [
    ...
    "ses:SendEmail",   # ← Resource = "*", no ses:FromAddress condition
    ...
  ]
  Resource = "*"
}

The deploy SA is a CI/CD service account; it needs to manage SES identities (Create/Delete/DKIM), but has no operational reason to send email. A compromised CI pipeline could use this unrestricted ses:SendEmail to send from any verified identity in the account. The Lambda runtime is already gated by ses:FromAddress StringLike *@${var.email_from_domain} — the deploy SA should either have ses:SendEmail removed entirely, or carry the same condition if there is a specific use case.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
terraform/environments/aws/ci-cd-permissions/policy_compute.tf (1)

263-276: Consider removing ses:SendEmail or adding ses:FromAddress condition.

The deploy SA includes ses:SendEmail with Resource = "*" and no ses:FromAddress condition. Per PR discussion, the CI deploy role likely doesn't need email-sending capability. If it does, adding the same ses:FromAddress restriction used by the Lambda runtime would prevent abuse:

Condition = {
  StringLike = {
    "ses:FromAddress" = "*@${var.email_from_domain}"
  }
}

Alternatively, split identity management actions from SendEmail and remove the latter from this policy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terraform/environments/aws/ci-cd-permissions/policy_compute.tf` around lines
263 - 276, The SES statement currently grants "ses:SendEmail" on Resource="*"
(the SES block with Sid = "SES" and Action including "ses:SendEmail"); either
remove "ses:SendEmail" from that Action list if the CI deploy role does not need
to send mail, or add a condition restricting the sender (mirror the Lambda
runtime rule) by adding a Condition with StringLike for "ses:FromAddress" =
"*@${var.email_from_domain}" to the same SES statement; alternatively split
identity-management actions (CreateEmailIdentity, DeleteEmailIdentity,
GetEmailIdentity, PutEmailIdentityDkimSigningAttributes, TagResource,
UntagResource) into a separate statement and omit "ses:SendEmail" from the CI
policy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@terraform/environments/aws/ci-cd-permissions/policy_compute.tf`:
- Around line 263-276: The SES statement currently grants "ses:SendEmail" on
Resource="*" (the SES block with Sid = "SES" and Action including
"ses:SendEmail"); either remove "ses:SendEmail" from that Action list if the CI
deploy role does not need to send mail, or add a condition restricting the
sender (mirror the Lambda runtime rule) by adding a Condition with StringLike
for "ses:FromAddress" = "*@${var.email_from_domain}" to the same SES statement;
alternatively split identity-management actions (CreateEmailIdentity,
DeleteEmailIdentity, GetEmailIdentity, PutEmailIdentityDkimSigningAttributes,
TagResource, UntagResource) into a separate statement and omit "ses:SendEmail"
from the CI policy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1b3bad3-6ba9-4b71-9dc2-027b976aba29

📥 Commits

Reviewing files that changed from the base of the PR and between eccfdf7 and 57fefbc.

📒 Files selected for processing (2)
  • terraform/environments/aws/ci-cd-permissions/policy_compute.tf
  • terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf

CR pass-3 nitpick on policy_compute.tf:271. The deploy SA's SES
statement listed the identity-management actions (Create/Delete/Get
EmailIdentity, PutDkimSigningAttributes, Tag/Untag) plus SendEmail —
the latter on Resource = "*" with no FromAddress condition.

Splitting actions is the principled fix: CI deploy configures email
identities (verify domain, rotate DKIM, tag); it doesn't send mail.
The runtime Lambda role in terraform/modules/compute/aws/lambda/main.tf
keeps its own ses:SendEmail grant (already restricted) for the actual
sending path. Dropping the CI grant means a leaked deploy SA can no
longer exfiltrate via mail.

A comment on the SES statement explains the omission so a future
reader doesn't add SendEmail back as a "missing action".

Closes the last open CR finding on PR #103.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 28, 2026

CR triage — round 3 (commit 18138b0eb)

CR pass-3 left 1 nitpick on policy_compute.tf:263-276ses:SendEmail in the CI deploy SA's SES statement. Of the three options CR offered (drop / add FromAddress condition / split actions), I picked drop SendEmail from CI:

  • The CI deploy SA configures email identities (CreateEmailIdentity, DeleteEmailIdentity, DKIM rotation, tag/untag) but does not need to send mail.
  • The runtime Lambda role (terraform/modules/compute/aws/lambda/main.tf:259) keeps its own ses:SendEmail grant — already restricted with the FromAddress = "*@${var.email_from_domain}" condition — for the actual sending path.
  • Dropping CI's SendEmail shrinks the blast radius of a leaked deploy SA: it can no longer exfiltrate via mail without unlocking any deploy capability.

A comment on the SES statement records the omission so a future reader doesn't add SendEmail back as a "missing action".

Deploy-SA terraform plan — deferred

Per the parent's instruction, I attempted terraform plan against terraform/environments/aws/ci-cd-permissions/ from the worktree to verify the IAM narrowing produces the expected diff. Skipping with the deferred-to-human marker:

  • Local AWS profiles available are scoped to unrelated workloads (karpenter-poc, ebc-*); aws sts get-caller-identity without --profile returns no default credentials.
  • Per the project CLAUDE.md §"CI/CD IAM — bootstrap vs runtime split", these modules are "applied once, manually, by a privileged human" — running plan with arbitrary creds against the CUDly deploy state isn't the right path.

Plan verification deferred to manual run by a privileged human before merge. The expected diff:

  • 5 IAM statements on aws_iam_policy.compute get ~ modifications: ECS, ECR, CloudFront, KMS, SES — each narrowing Resource = "*" (or removing actions where appropriate).
  • The KMS narrowing splits into two new statements (KMSMutateTaggedOnly and KMSAliasMutate) per CR pass-2 feedback.
  • No resource creations or deletions expected — all changes are policy-document modifications on the existing aws_iam_policy and aws_iam_role resources.

If you'd like the agent to re-attempt the plan with a specific named profile, share the profile name (or instruct via the CUDly deploy SA's name) and I can re-run.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
SHA="18138b0eb"
for f in \
  "terraform/environments/aws/ci-cd-permissions/policy_compute.tf" \
  "terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf" \
  "terraform/modules/compute/aws/lambda/main.tf"; do
  echo "===== $f ====="
  gh api "repos/LeanerCloud/CUDly/contents/$f?ref=$SHA" --jq '.content' | base64 -d
  echo ""
done

Length of output: 30808


@cristim — I've read policy_compute.tf, policy_compute_b.tf, and lambda/main.tf at 18138b0eb. Round-3 fix is confirmed correct. Here's the full closure summary:


Round-3 review — all findings resolved ✅

🟢 ses:SendEmail removed from deploy SA — confirmed correct

The SES statement in policy_compute.tf now contains only identity-management actions:

CreateEmailIdentity, DeleteEmailIdentity, GetEmailIdentity,
PutEmailIdentityDkimSigningAttributes, TagResource, UntagResource

ses:SendEmail is gone, and the inline comment records the intentional omission with a clear rationale — a future reader won't accidentally restore it. The Lambda runtime role retains its own ses:SendEmail / ses:SendRawEmail correctly gated by ses:FromAddress StringLike *@${var.email_from_domain} in SendFromCUDlyDomain. The blast radius of a leaked deploy SA is now meaningfully smaller.


Complete findings closure across all three rounds

Round Finding Statement Status
1 CloudFront function actions not scopeable via aws:ResourceTag CloudFrontFnMutatearn:aws:cloudfront::*:function/cudly-*
1 ecs:ListTasks ignores resource-level ARN scoping ECSListTasks with ArnLike ecs:cluster condition
1 KMS alias tag-gate is a no-op (aliases have no IAM-visible tags) KMSAliasMutate ARN-scoped to alias/cudly-*
2 KMS alias operations require dual-resource authorization (alias + key) Key-side added to KMSMutateTaggedOnly (tag-gated); alias-side in KMSAliasMutate (ARN-scoped)
3 Deploy SA ses:SendEmail with Resource="*", no FromAddress condition Action removed from CI SA entirely

No open findings remain. The PR is clean and ready to merge — pending the manual terraform apply of terraform/environments/aws/ci-cd-permissions/ by a privileged human post-merge, as noted.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
terraform/environments/aws/ci-cd-permissions/policy_compute.tf (1)

117-128: Split CloudFront function reads out of the wildcard statement.

Line 122 and Line 125 do not need to stay under Resource = "*" just because Line 121 does. In the CloudFront authorization reference, DescribeFunction and GetFunction are resource-scopeable on function*, while CreateFunction has no resource type and therefore must remain wildcarded. Keeping all three together still lets the deploy role read arbitrary CloudFront function config/code in the account. Move just the two read actions into their own statement scoped to arn:aws:cloudfront::*:function/cudly-*. (docs.aws.amazon.com)

Proposed change
       {
         Sid    = "CloudFrontCreateAndRead"
         Effect = "Allow"
         Action = [
           "cloudfront:CreateDistribution",
           "cloudfront:CreateFunction",
-          "cloudfront:DescribeFunction",
           "cloudfront:GetDistribution",
           "cloudfront:GetDistributionConfig",
-          "cloudfront:GetFunction",
           "cloudfront:ListTagsForResource",
         ]
         Resource = "*"
       },
+      {
+        Sid    = "CloudFrontFnRead"
+        Effect = "Allow"
+        Action = [
+          "cloudfront:DescribeFunction",
+          "cloudfront:GetFunction",
+        ]
+        Resource = "arn:aws:cloudfront::*:function/cudly-*"
+      },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terraform/environments/aws/ci-cd-permissions/policy_compute.tf` around lines
117 - 128, The CloudFront statement "CloudFrontCreateAndRead" currently groups
CreateFunction (requires Resource="*") with DescribeFunction and GetFunction
(which are resource-scoped); split this into two statements: keep a statement
containing "cloudfront:CreateFunction" with Resource="*", and create a new
statement containing "cloudfront:DescribeFunction" and "cloudfront:GetFunction"
scoped to Resource "arn:aws:cloudfront::*:function/cudly-*"; update the Sid
values (e.g., "CloudFrontCreate" and "CloudFrontReadFunctions") so the policy
clearly separates the wildcard create permission from the scoped read
permissions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@terraform/environments/aws/ci-cd-permissions/policy_compute.tf`:
- Around line 117-128: The CloudFront statement "CloudFrontCreateAndRead"
currently groups CreateFunction (requires Resource="*") with DescribeFunction
and GetFunction (which are resource-scoped); split this into two statements:
keep a statement containing "cloudfront:CreateFunction" with Resource="*", and
create a new statement containing "cloudfront:DescribeFunction" and
"cloudfront:GetFunction" scoped to Resource
"arn:aws:cloudfront::*:function/cudly-*"; update the Sid values (e.g.,
"CloudFrontCreate" and "CloudFrontReadFunctions") so the policy clearly
separates the wildcard create permission from the scoped read permissions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0933f105-e9e2-4be8-9134-7fd5f8236e15

📥 Commits

Reviewing files that changed from the base of the PR and between 57fefbc and 18138b0.

📒 Files selected for processing (1)
  • terraform/environments/aws/ci-cd-permissions/policy_compute.tf

@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 29, 2026

@coderabbitai create a commit to address this nitpick for terraform/environments/aws/ci-cd-permissions/policy_compute.tf (1)
117-128: Split CloudFront function reads out of the wildcard statement.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me read the current state of the file first.


🏁 Script executed:

#!/bin/bash
SHA=$(gh api repos/LeanerCloud/CUDly/pulls/103 --jq '.head.sha')
echo "HEAD SHA: $SHA"
echo "===== policy_compute.tf ====="
gh api "repos/LeanerCloud/CUDly/contents/terraform/environments/aws/ci-cd-permissions/policy_compute.tf?ref=$SHA" --jq '.content' | base64 -d

Length of output: 12715


I can see the issue: CloudFrontCreateAndRead bundles function reads (CreateFunction, DescribeFunction, GetFunction) into Resource="*", but CloudFront function ARNs follow a known pattern (arn:aws:cloudfront::*:function/<name>) — unlike distribution ARNs which are auto-generated. These can be ARN-scoped to cudly-* just like the mutate actions already are in policy_compute_b.tf.

Let me read policy_compute_b.tf to plan the insertion point cleanly.


🏁 Script executed:

#!/bin/bash
SHA="18138b0ebc8ad67ecba3f39a8be966fa0d5f701e"
echo "===== policy_compute_b.tf ====="
gh api "repos/LeanerCloud/CUDly/contents/terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf?ref=$SHA" --jq '.content' | base64 -d

Length of output: 2456


Got everything I need. Here's the plan:

  • Remove CreateFunction, DescribeFunction, GetFunction from the wildcard CloudFrontCreateAndRead statement in policy_compute.tf and update its comment/Sid
  • Add a new CloudFrontFnRead statement in policy_compute_b.tf scoped to arn:aws:cloudfront::*:function/cudly-* (alongside the existing CloudFrontFnMutate) and update the file's header comment

  • 📌 Commit the changes to this branch

DescribeFunction and GetFunction support the function resource type
per the AWS Service Authorization Reference. Move them out of the
wildcard CloudFrontCreateAndRead statement into a new
CloudFrontFnRead statement scoped to
arn:aws:cloudfront::*:function/cudly-*, matching the ARN scoping
already used by CloudFrontFnMutate in policy_compute_b.tf.
CreateFunction has no resource type so stays under Resource="*".
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 29, 2026

Addressed CR r4 nitpick in 2a67339:

  • Split CloudFront function reads out of CloudFrontCreateAndRead: applied as suggested. Moved cloudfront:DescribeFunction and cloudfront:GetFunction into a new CloudFrontFnRead statement scoped to arn:aws:cloudfront::*:function/cudly-*, matching the ARN scoping already used by CloudFrontFnMutate in policy_compute_b.tf. cloudfront:CreateFunction has no resource type per the AWS Service Authorization Reference and stays under Resource = "*".

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me read the files at the new commit to verify.


🏁 Script executed:

#!/bin/bash
SHA="2a67339"
for f in \
  "terraform/environments/aws/ci-cd-permissions/policy_compute.tf" \
  "terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf"; do
  echo "===== $f ====="
  gh api "repos/LeanerCloud/CUDly/contents/$f?ref=$SHA" --jq '.content' | base64 -d
  echo ""
done

Length of output: 15861


Let me verify one detail before finalizing the review.


🌐 Web query:

AWS CloudFront CreateFunction IAM resource type Service Authorization Reference

💡 Result:

In the Amazon CloudFront – Service Authorization Reference, the IAM action cloudfront:CreateFunction has no resource type listed in the “Resource types” column. That means it does not support resource-level permissions, so an IAM statement allowing cloudfront:CreateFunction must use:

  • Resource: "*" (you can’t scope it to a specific function ARN at create time). [1]

CloudFront does define a function resource type (used by other actions like GetFunction, UpdateFunction, DeleteFunction, etc.) with ARN format:

  • arn:${Partition}:cloudfront::${Account}:function/${Name} [2]

Sources: [1] [2]


@cristim — verified the fix at 2a67339. Implementation is correct and actually tighter than my suggested plan.


Round-4 nitpick — confirmed ✅

Action Statement Resource Rationale
cloudfront:CreateFunction CloudFrontCreateAndRead "*" No resource type in AWS SAR — cannot be ARN-scoped
cloudfront:DescribeFunction CloudFrontFnRead arn:aws:cloudfront::*:function/cudly-* Accepts function ARN → scopeable ✅
cloudfront:GetFunction CloudFrontFnRead arn:aws:cloudfront::*:function/cudly-* Accepts function ARN → scopeable ✅

My earlier plan incorrectly suggested CreateFunction could be ARN-scoped alongside the other two. The AWS Service Authorization Reference confirms it has no resource type listed, so Resource="*" is the only valid option. The CloudFrontFnRead comment in policy_compute.tf accurately documents this distinction.

The policy_compute_b.tf header comment is also consistent — since CloudFrontFnRead ended up in policy_compute.tf directly (not in the split-out policy), it correctly doesn't appear in the compute_b comment block.


Full closure — all 5 rounds resolved ✅

Round Finding Status
1 CloudFront function actions not scopeable via aws:ResourceTag CloudFrontFnMutate ARN-scoped
1 ecs:ListTasks ignores resource-level ARN scoping ECSListTasks with ArnLike ecs:cluster
1 KMS alias tag-gate is a no-op KMSAliasMutate ARN-scoped
2 KMS alias ops require dual-resource authorization ✅ Key-side in KMSMutateTaggedOnly, alias-side in KMSAliasMutate
3 Deploy SA ses:SendEmail with no condition ✅ Removed from CI SA
4 DescribeFunction/GetFunction under wildcard resource CloudFrontFnRead scoped to cudly-*

No open findings remain. The PR is clean and ready to merge — pending the manual terraform apply of terraform/environments/aws/ci-cd-permissions/ post-merge.

@cristim cristim merged commit 845a559 into feat/multicloud-web-frontend Apr 29, 2026
3 checks passed
@cristim cristim deleted the fix/iam-resource-wildcards branch April 29, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/all-users Affects every user priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/security Security finding urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant