An IAM role on one side, a denied ses:SendEmail action on the other, and the missing policy in the gap between them.

Amazon SES: "User Is Not Authorized to Perform ses:SendEmail" (The IAM Fix)


Video walkthrough A short version of this guide. Full written version below.

You ran your code. Nothing sent. Instead you got this:

User: arn:aws:sts::123456789012:assumed-role/my-app-role/… is not authorized to perform: ses:SendEmail

Here’s the good news: your code is fine. This is a permissions error, and it’s a five-minute fix — once you know the two things almost everyone gets wrong.


First: What This Error Actually Means

This is an IAM error, not a code error and not a verification error. The identity your application runs as doesn’t have permission to call SES.

AWS denies everything by default. Nothing can do anything until a policy explicitly allows it. So “not authorized to perform ses:SendEmail” is AWS telling you, precisely, which action got blocked — and it’s up to you to grant it.


The Fix: One Policy

Attach a policy that allows ses:SendEmail — and ses:SendRawEmail too, if you send raw MIME or attachments through older SDKs:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ses:SendEmail",
        "ses:SendRawEmail"
      ],
      "Resource": "*"
    }
  ]
}

One note on those two actions: ses:SendEmail covers both the v1 and v2 SendEmail APIs, and ses:SendRawEmail covers raw MIME and older SDKs. Those two together is the right pair for almost every app.

You can either paste this JSON in IAM’s policy editor, or build the same thing in the visual editor (pick SES as the service, check SendEmail and SendRawEmail). The JSON is just faster and unambiguous.


The Part Everyone Gets Wrong: Attach It to the Right Identity

This is where most of the wasted hours go. The policy has to go on the identity your app actually runs as — not your personal IAM user.

  • On Lambda, that’s the function’s execution role.
  • On EC2, it’s the instance role.
  • In a container (ECS/Fargate), it’s the task role.

And you don’t have to guess which one — the error names it. Read the ARN:

…assumed-role/my-app-role/…

That’s the identity that got denied. Attach the policy to my-app-role, and the error goes away.


Keep It Least-Privilege

It’s tempting to reach for the managed AmazonSESFullAccess policy and move on. Don’t. That grants far more than sending — it’s the opposite of least-privilege.

Two actions is all you need. If you want to lock it down further, scope the policy to a single verified identity and even a specific From address:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["ses:SendEmail", "ses:SendRawEmail"],
      "Resource": "arn:aws:ses:us-east-1:123456789012:identity/yourdomain.com",
      "Condition": {
        "StringEquals": { "ses:FromAddress": "noreply@yourdomain.com" }
      }
    }
  ]
}

Swap in your account ID, region, domain, and From address.


The One Thing Nobody Tells You: Permissions and the Sandbox Are Two Different Gates

People constantly conflate this error with the SES sandbox. They’re not the same thing, and confusing them sends you fixing the wrong one.

  • This error is about whether you’re allowed to send at all — an IAM permissions question.
  • The sandbox is about who you’re allowed to send to — verified addresses only, and a low daily cap.

They’re independent gates. Fixing your permissions won’t get you out of the sandbox. Leaving the sandbox won’t grant your role permission. If you’re hitting both walls, you fix them separately — the policy above for one, a production-access request for the other.


Quick Checklist

  • Read the ARN in the error — that names the identity.
  • Attach a policy allowing ses:SendEmail (and ses:SendRawEmail).
  • Put it on your app’s role, not your personal user.
  • Keep it to those two actions — skip AmazonSESFullAccess.
  • Still blocked after that? Check you’re operating in the right region and that the identity is verified — separate errors, same discipline: read what AWS is telling you and fix that exact thing.

The Pattern Underneath

Nearly every “SES won’t send” problem is AWS denying something by default and telling you precisely what. The fix is almost always the same two-part question: which identity, and which action. Read the error, answer those two, and you’re sending.