Back to Checks
aws

SES identity resource policy does not allow public access

ses_identity_not_publicly_accessible
Service: sesSeverity: high
OfficialBuilt-inVerified
Categories:internet-exposed
Resource Type:AwsIamPolicy

Amazon SES identities are evaluated for publicly accessible resource policies-for example, statements with Principal:"*" or broadly trusted principals that permit actions against the identity.

Risk

Public SES identity policies allow unauthorized email sending or configuration changes.

  • Integrity: spoofed emails and brand impersonation
  • Confidentiality: exposure of identity details
  • Availability: reputation loss causing throttling or suspension

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks ses_identity_not_publicly_accessible

Recommendation

Restrict SES identity policies to known principals and actions following least privilege. Prefer explicit account ARNs for sending authorization, and add conditions like aws:SourceIp and aws:SecureTransport. Review grants regularly and remove unused access as part of defense in depth.

Remediation

CLI

aws sesv2 delete-email-identity-policy --email-identity <IDENTITY-NAME> --policy-name <POLICY-NAME>

Terraform

resource "aws_ses_identity_policy" "<example_resource_name>" {
  identity = "<example_resource_name>"
  name     = "<example_resource_name>"

  policy = jsonencode({
    Version   = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = ["<account_id>"] } # Critical: restrict to a specific AWS account, not "*"
      Action    = ["ses:SendEmail"]
      Resource  = "arn:aws:ses:<region>:<account_id>:identity/<example_resource_name>"
    }]
  })
}

Other

1In the AWS Console, go to Simple Email Service (SES)
2Open Verified identities and select the affected identity
3Click Resource policies
4Delete the public policy, or Edit it to remove any Principal of "*" and restrict to a specific AWS account
5Save changes

Check Code

from copy import deepcopy

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.iam.lib.policy import is_policy_public
from prowler.providers.aws.services.ses.ses_client import ses_client


def _normalize_policy_statements(policy: dict) -> dict:
    statements = policy.get("Statement", [])
    if isinstance(statements, dict):
        return {**policy, "Statement": [statements]}
    return policy


def _has_explicit_deny(policy: dict) -> bool:
    return any(
        isinstance(statement, dict) and statement.get("Effect") == "Deny"
        for statement in _normalize_policy_statements(policy).get("Statement", [])
    )


class ses_identity_not_publicly_accessible(Check):
    """Ensure SES identities are not publicly accessible through authorization policies."""

    def execute(self) -> list[Check_Report_AWS]:
        """Evaluate every authorization policy attached to each SES identity.

        Returns:
            A list of reports containing the public-access result for each identity.
        """
        findings = []
        for identity in ses_client.email_identities.values():
            if not identity.policies:
                continue
            report = Check_Report_AWS(metadata=self.metadata(), resource=identity)
            report.status = "PASS"
            report.status_extended = (
                f"SES identity {identity.name} is not publicly accessible."
            )
            has_public_allow = any(
                is_policy_public(
                    _normalize_policy_statements(deepcopy(policy)),
                    ses_client.audited_account,
                )
                for policy in identity.policies.values()
            )
            if has_public_allow:
                if any(
                    _has_explicit_deny(policy) for policy in identity.policies.values()
                ):
                    report.status = "MANUAL"
                    report.status_extended = f"SES identity {identity.name} has public Allow and explicit Deny statements in its resource policies. Effective public access requires manual review."
                else:
                    report.status = "FAIL"
                    report.status_extended = f"SES identity {identity.name} is publicly accessible due to its resource policies."

            findings.append(report)

        return findings