Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 135 additions & 26 deletions terraform/environments/aws/ci-cd-permissions/policy_compute.tf
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ resource "aws_iam_policy" "compute" {
Resource = "arn:aws:events:*:*:rule/cudly-*"
},
{
Sid = "ECR"
# ECR repository-scoped actions: every action that takes a
# repository ARN is restricted to cudly-* repositories. This
# prevents the deploy SA from poisoning, deleting, or exfiltrating
# images in unrelated ECR repositories sharing the same account.
Sid = "ECRRepositoryScoped"
Effect = "Allow"
Action = [
"ecr:BatchCheckLayerAvailability",
Expand All @@ -81,7 +85,6 @@ resource "aws_iam_policy" "compute" {
"ecr:DeleteRepository",
"ecr:DeleteRepositoryPolicy",
"ecr:DescribeRepositories",
"ecr:GetAuthorizationToken",
"ecr:GetDownloadUrlForLayer",
"ecr:GetLifecyclePolicy",
"ecr:GetRepositoryPolicy",
Expand All @@ -96,28 +99,73 @@ resource "aws_iam_policy" "compute" {
"ecr:UntagResource",
"ecr:UploadLayerPart",
]
Resource = "arn:aws:ecr:*:*:repository/cudly-*"
},
{
# ECR token endpoint — does not take a resource ARN and is
# account-wide by design. Required for `docker login` against ECR.
Sid = "ECRGetAuthorizationToken"
Effect = "Allow"
Action = ["ecr:GetAuthorizationToken"]
Resource = "*"
},
{
Sid = "CloudFrontFrontend"
# CloudFront create + distribution-read actions don't take a
# resource ARN at the API level (a distribution's ARN is only
# known after create, and CreateFunction has no resource type
# per the AWS Service Authorization Reference). Read actions
# against distributions are also broad because terraform plan
# needs to enumerate resources. Mutating actions are gated
# below; function reads are scoped in CloudFrontFnRead.
Sid = "CloudFrontCreateAndRead"
Effect = "Allow"
Action = [
"cloudfront:CreateDistribution",
"cloudfront:CreateFunction",
"cloudfront:DeleteDistribution",
"cloudfront:DeleteFunction",
"cloudfront:DescribeFunction",
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
"cloudfront:GetFunction",
"cloudfront:ListTagsForResource",
"cloudfront:PublishFunction",
]
Resource = "*"
},
{
# CloudFront DescribeFunction/GetFunction support the
# function resource type per the AWS Service Authorization
# Reference, so scope them to CUDly-owned functions to match
# the ARN scoping used by CloudFrontFnMutate in
# policy_compute_b.tf.
Sid = "CloudFrontFnRead"
Effect = "Allow"
Action = [
"cloudfront:DescribeFunction",
"cloudfront:GetFunction",
]
Resource = "arn:aws:cloudfront::*:function/cudly-*"
},
{
# CloudFront distribution mutations are restricted to distributions
# tagged Project=CUDly. The Terraform aws_cloudfront_distribution
# resource sets this tag at creation time (see modules/frontend/aws),
# so subsequent Update/Delete/Tag/Untag operations against
# CUDly-owned distributions succeed while attempts to mutate any
# third-party distribution sharing the account are denied.
# Function mutations live in policy_compute_b.tf — the function
# resource type does not support aws:ResourceTag per the AWS
# Service Authorization Reference, so it needs ARN scoping.
Sid = "CloudFrontMutateTaggedOnly"
Effect = "Allow"
Action = [
"cloudfront:DeleteDistribution",
"cloudfront:TagResource",
"cloudfront:UntagResource",
"cloudfront:UpdateDistribution",
"cloudfront:UpdateFunction",
]
Resource = "*"
Condition = {
StringEquals = {
"aws:ResourceTag/Project" = "CUDly"
}
}
Comment on lines +155 to +168
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.

},
{
Sid = "CloudWatchAlarms"
Expand All @@ -133,29 +181,58 @@ resource "aws_iam_policy" "compute" {
Resource = "arn:aws:cloudwatch:*:*:alarm:cudly-*"
},
{
Sid = "ECSFargate"
# ECS resource-scoped actions: cluster, service, and task ARNs
# all match cudly-* prefix. RegisterTaskDefinition cannot take a
# specific ARN at registration time and is split below.
# ecs:ListTasks does NOT support resource-level permissions per
# the AWS Service Authorization Reference; it lives in
# policy_compute_b.tf gated by an ecs:cluster condition.
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-*:*",
]
Comment on lines +190 to +211
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-*".

},
{
# ECS task-definition + tag-listing actions don't take a specific
# ARN at API level. RegisterTaskDefinition has no resource;
# DeregisterTaskDefinition + DescribeTaskDefinition take a
# task-definition ARN that we constrain to cudly-*.
# ListTagsForResource takes any ARN type but is read-only.
Sid = "ECSFargateAccountWide"
Effect = "Allow"
Action = [
"ecs:RegisterTaskDefinition",
"ecs:ListTagsForResource",
]
Resource = "*"
},
{
Sid = "ECSFargateTaskDefinition"
Effect = "Allow"
Action = [
"ecs:DeregisterTaskDefinition",
"ecs:DescribeTaskDefinition",
]
Resource = "arn:aws:ecs:*:*:task-definition/cudly-*:*"
},
{
Sid = "ELBFargate"
Effect = "Allow"
Expand Down Expand Up @@ -199,38 +276,65 @@ resource "aws_iam_policy" "compute" {
Resource = "*"
},
{
# CI deploy SA configures email identities (verify domain,
# rotate DKIM, tag) but does NOT send mail — that path lives
# in the Lambda runtime role (modules/compute/aws/lambda).
# ses:SendEmail intentionally excluded here per CR pass-3
# nitpick — adding it would let a leaked deploy SA exfiltrate
# via mail without unlocking any deploy capability.
Sid = "SES"
Effect = "Allow"
Action = [
"ses:CreateEmailIdentity",
"ses:DeleteEmailIdentity",
"ses:GetEmailIdentity",
"ses:PutEmailIdentityDkimSigningAttributes",
"ses:SendEmail",
"ses:TagResource",
"ses:UntagResource",
]
Resource = "*"
},
{
# KMS asymmetric signing key for the CUDly OIDC issuer.
# Broad KMS management perms scoped to Describe/Tag/Alias
# actions required by terraform-aws-provider on key create
# and update. Resource is "*" because KMS key ARNs are only
# known after creation.
Sid = "KMSSigningKey"
# KMS create + read-only actions don't accept a key ARN at API
# level (key ARNs are only known after CreateKey). Read-only
# actions are also broad because terraform plan needs to
# enumerate key state.
# kms:CreateAlias is NOT here — it moved to KMSMutateTaggedOnly
# below (key-side check, tag-gated) and policy_compute_b.tf
# KMSAliasMutate (alias-side check, ARN-scoped). KMS evaluates
# alias actions against both the alias and the target key.
Sid = "KMSCreateAndRead"
Effect = "Allow"
Action = [
"kms:CreateAlias",
"kms:CreateKey",
"kms:DeleteAlias",
"kms:DescribeKey",
"kms:DisableKey",
"kms:EnableKey",
"kms:GetKeyPolicy",
"kms:GetKeyRotationStatus",
"kms:ListAliases",
"kms:ListResourceTags",
]
Resource = "*"
},
{
# KMS mutating + destructive actions are gated on the key being
# tagged Project=CUDly. The CUDly OIDC signing key is tagged at
# creation by Terraform (see modules/security/aws/kms_signing.tf).
# Without this gate the deploy SA could schedule deletion or
# disable any KMS key in the account, causing denial of service
# for unrelated workloads.
# Alias mutations (CreateAlias/DeleteAlias/UpdateAlias) appear
# here because AWS KMS evaluates these actions against BOTH the
# alias and the target key resource — this statement covers the
# key-side check (tag-gated). The alias-side check lives in
# policy_compute_b.tf KMSAliasMutate (ARN-scoped because aliases
# have no IAM-visible tags).
Sid = "KMSMutateTaggedOnly"
Effect = "Allow"
Action = [
"kms:CreateAlias",
"kms:DeleteAlias",
"kms:DisableKey",
"kms:EnableKey",
"kms:PutKeyPolicy",
"kms:ScheduleKeyDeletion",
"kms:TagResource",
Expand All @@ -239,6 +343,11 @@ resource "aws_iam_policy" "compute" {
"kms:UpdateKeyDescription",
]
Resource = "*"
Condition = {
StringEquals = {
"aws:ResourceTag/Project" = "CUDly"
}
}
},
]
})
Expand Down
72 changes: 72 additions & 0 deletions terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Second compute-permissions policy. This file exists because the main
# `aws_iam_policy.compute` is at AWS's 6144-char managed-policy limit;
# additional ARN-scoped statements (added in response to a CodeRabbit
# review of fix/iam-resource-wildcards) wouldn't fit there without
# evicting unrelated existing grants. Each statement here corrects a
# scoping bug that the AWS service authorization reference forced us to
# split out of `policy_compute.tf`:
#
# - CloudFrontFnMutate: cloudfront:DeleteFunction/PublishFunction/
# UpdateFunction operate on the `function` resource type which does
# not support aws:ResourceTag, so tag-gating them is a no-op. ARN
# scope used instead.
#
# - ECSListTasks: ecs:ListTasks does not support resource-level
# permissions, so Resource must be "*". The ecs:cluster condition
# key restricts it to CUDly clusters.
#
# - KMSAliasMutate: kms:CreateAlias/DeleteAlias/UpdateAlias act on
# alias resources, but aws:ResourceTag/Project gates only key
# resources (aliases inherit nothing). KMS evaluates these actions
# against BOTH the alias and the target key, so the alias-side
# check lives here (ARN-scoped) and the key-side check lives in
# policy_compute.tf KMSMutateTaggedOnly (tag-gated to CUDly keys).

resource "aws_iam_policy" "compute_b" {
name = "cudly-deploy-compute-b"
description = "CUDly Terraform deploy: ARN-scoped statements split out of compute policy due to size"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "CloudFrontFnMutate"
Effect = "Allow"
Action = [
"cloudfront:DeleteFunction",
"cloudfront:PublishFunction",
"cloudfront:UpdateFunction",
]
Resource = "arn:aws:cloudfront::*:function/cudly-*"
},
{
Sid = "ECSListTasks"
Effect = "Allow"
Action = [
"ecs:ListTasks",
]
Resource = "*"
Condition = {
ArnLike = {
"ecs:cluster" = "arn:aws:ecs:*:*:cluster/cudly-*"
}
}
},
{
Sid = "KMSAliasMutate"
Effect = "Allow"
Action = [
"kms:CreateAlias",
"kms:DeleteAlias",
"kms:UpdateAlias",
]
Resource = "arn:aws:kms:*:*:alias/cudly-*"
Comment on lines +56 to +63
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.

},
]
})

tags = {
Project = "CUDly"
ManagedBy = "terraform"
}
}
5 changes: 5 additions & 0 deletions terraform/environments/aws/ci-cd-permissions/role.tf
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ resource "aws_iam_role_policy_attachment" "compute" {
policy_arn = aws_iam_policy.compute.arn
}

resource "aws_iam_role_policy_attachment" "compute_b" {
role = aws_iam_role.cudly_deploy.name
policy_arn = aws_iam_policy.compute_b.arn
}

resource "aws_iam_role_policy_attachment" "data" {
role = aws_iam_role.cudly_deploy.name
policy_arn = aws_iam_policy.data.arn
Expand Down
30 changes: 25 additions & 5 deletions terraform/modules/compute/aws/lambda/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,16 @@ resource "aws_iam_role_policy" "secrets_access" {
# where SES walks up the identity hierarchy and evaluates IAM against the
# verified parent's ARN (identity/leanercloud.com). Enumerating every
# ancestor the operator might verify would work on paper but drifts every
# time the SES identity tree is reorganised; `*` defers the real check to
# SES itself (which still rejects sends from unverified identities at the
# service layer). Configuration-set access stays scoped to ${stack_name}*
# so a compromised Lambda can't touch unrelated stacks' config sets.
# time the SES identity tree is reorganised.
#
# To still narrow the blast radius (a compromised Lambda must not be able
# to spoof emails from arbitrary verified identities in the same account),
# the SendEmail/SendRawEmail actions are gated by a `ses:FromAddress`
# condition restricting the From header to *@${var.email_from_domain}.
# SES enforces FromAddress on the wire, so this prevents phishing from
# unrelated identities even though the resource ARN remains broad.
# Configuration-set access stays scoped to ${stack_name}* so a compromised
# Lambda can't touch unrelated stacks' config sets either.
#
# Only attached when var.email_from_domain is set — deployments without email
# notifications don't get any SES permissions at all.
Expand All @@ -247,11 +253,25 @@ resource "aws_iam_role_policy" "ses_access" {
Version = "2012-10-17"
Statement = [
{
Sid = "SendFromAnyVerifiedIdentity"
Sid = "SendFromCUDlyDomain"
Effect = "Allow"
Action = [
"ses:SendEmail",
"ses:SendRawEmail",
]
Resource = "*"
Condition = {
StringLike = {
"ses:FromAddress" = "*@${var.email_from_domain}"
}
}
},
{
# Read-only SES status checks for healthchecks and quota
# introspection. No spoofing risk — these don't send mail.
Sid = "SESReadOnly"
Effect = "Allow"
Action = [
"ses:GetAccount",
"ses:GetEmailIdentity",
]
Expand Down