Skip to content

IAM policies

SESKit must never ask for AdministratorAccess. There are two policies because they grant genuinely different things, and you should be able to run SESKit without the second.

Sending — seven actions (required)

Everything except delivery events. All of it is scoped to SES, and only one action can create anything.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "sts:GetCallerIdentity",
        "ses:GetAccount",
        "ses:CreateEmailIdentity",
        "ses:GetEmailIdentity",
        "ses:DeleteEmailIdentity",
        "ses:SendEmail",
        "ses:PutAccountDetails"
      ],
      "Resource": "*"
    }
  ]
}

The first two are read-only and are all that connecting an account needs, so you can grant just those to look around before committing to anything. The identity actions create and remove the verified senders on the Domains page.

ses:SendEmail is the one to grant deliberately: it is the only action here that can reach the outside world and appear on your AWS bill. You do not need it to try SESKit — without an AWS connection, sending goes to Mailpit locally.

Removing an identity is the only destructive thing here, and it is guarded: SESKit deletes the identity in SES only when no other project is still using it.

ses:PutAccountDetails is what requesting production access from the AWS page uses. It changes nothing about the account by itself - it files the request AWS reviews - and a key without it is told so, by name, when the button is pressed. A key made before this line was here still connects and sends; add the action and the request works.

Delivery events — nine more (optional)

Read this one before granting it

Everything above is scoped to SES and mostly read-only. This adds permission to create and delete SNS topics and SQS queues in your account — a different kind of trust, and the reason it is a separate policy and a separate button rather than something the connect flow quietly needs.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "sqs:CreateQueue",
        "sqs:GetQueueAttributes",
        "sqs:SetQueueAttributes",
        "sqs:DeleteQueue",
        "sqs:ReceiveMessage",
        "sqs:DeleteMessage"
      ],
      "Resource": "arn:aws:sqs:*:*:seskit-events"
    },
    {
      "Effect": "Allow",
      "Action": [
        "sns:CreateTopic",
        "sns:Subscribe",
        "sns:Unsubscribe",
        "sns:DeleteTopic"
      ],
      "Resource": "arn:aws:sns:*:*:seskit-events"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ses:CreateConfigurationSet",
        "ses:DeleteConfigurationSet",
        "ses:CreateConfigurationSetEventDestination",
        "ses:UpdateConfigurationSetEventDestination",
        "ses:DeleteConfigurationSetEventDestination"
      ],
      "Resource": "*"
    }
  ]
}

The SQS and SNS statements are scoped by resource to the queue and topic SESKit creates, so this policy cannot touch anything else you own even by mistake. If you change EVENT_RESOURCE_PREFIX, change those ARNs to match.

The delete permissions are there so that removing event reporting, or disconnecting the account, actually cleans up. Granting create without delete would leave SESKit able to make resources in your account and unable to tidy them away.

sqs:SendMessage is deliberately absent. SESKit never writes to the queue — SNS does, under a queue policy SESKit sets during setup that admits that one topic and nothing else. Adding it here would grant a permission nothing uses.

Starting smaller

A reasonable order, if you would rather not grant everything at once:

Stage Grant You can then
Look around sts:GetCallerIdentity, ses:GetAccount Connect, see your quota and sandbox status
Verify senders + ses:*EmailIdentity Add domains and addresses
Send for real + ses:SendEmail Deliver through SES
Leave the sandbox + ses:PutAccountDetails Request production access from the AWS page
See what happened the second policy Delivery events and webhooks

Each stage is usable on its own, and the dashboard says what is missing rather than failing obscurely.