Email - One SES user and one domain identity per app
Running several RailsFast apps on one AWS account? Give each app its own IAM user and its own SES domain identity. Then a leaked key of one app sends mail only as that app's domain, and you rotate or delete it without touching the others. This page does the whole setup from the terminal: aws for SES and IAM, and the Cloudflare CLI (cf) for the DNS records.
The examples use the domain example.com, the region us-east-1 and the app name myapp. Replace them with yours.
1. The IAM user, allowed to send only as its domain
RailsFast sends mail with aws-actionmailer-ses (delivery_method = :ses), which calls ses:SendRawEmail. Allow that action on this app's identity only. Save this as ses-send-myapp.json, with your account id in place of 111122223333:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ses:SendRawEmail", "ses:SendEmail"],
"Resource": "arn:aws:ses:us-east-1:111122223333:identity/example.com"
}
]
}
aws iam create-user --user-name myapp-production
aws iam put-user-policy --user-name myapp-production \
--policy-name ses-send-example-com --policy-document file://ses-send-myapp.json
# The key goes straight into a file that only you can read. Never echo it.
umask 077
aws iam create-access-key --user-name myapp-production \
--query 'AccessKey.[AccessKeyId,SecretAccessKey]' --output text > /tmp/myapp-ses.key
Put the two values into the production → aws block of your credentials (see Configuration), then delete /tmp/myapp-ses.key.
The broader policy on the Configuration page ("Resource": "*") also works. The policy above is narrower: this user cannot send as any other domain of the account. While the account is still in the SES sandbox, AWS also checks the recipient addresses, so test with verified recipients.
When you turn on bounce and complaint handling, every mail names an SES configuration set, and SES then checks that set as a resource too. Add arn:aws:ses:us-east-1:111122223333:configuration-set/myapp-mail to the Resource of this policy before you deploy that change, or SES refuses every mail.
2. The SES domain identity
aws sesv2 create-email-identity --email-identity example.com \
--dkim-signing-attributes NextSigningKeyLength=RSA_2048_BIT --region us-east-1
aws sesv2 put-email-identity-mail-from-attributes --email-identity example.com \
--mail-from-domain mail.example.com --behavior-on-mx-failure USE_DEFAULT_VALUE --region us-east-1
# The three DKIM tokens, for the next step:
TOKENS=$(aws sesv2 get-email-identity --email-identity example.com --region us-east-1 \
--query 'DkimAttributes.Tokens' --output text)
The custom MAIL FROM domain (mail.example.com) makes the bounce address your domain, so SPF aligns for DMARC. USE_DEFAULT_VALUE keeps mail flowing on Amazon's MAIL FROM if your MX record ever breaks.
3. The DNS records, with cf
Log in once with cf auth login (or a profile with Zone → DNS → Edit on the zone). All these records are DNS only: proxied must be false.
# DKIM: three CNAMEs
for t in $TOKENS; do
cf dns records create -z example.com --body "{\"type\":\"CNAME\",\"name\":\"$t._domainkey.example.com\",\"content\":\"$t.dkim.amazonses.com\",\"proxied\":false,\"ttl\":1}"
done
# MAIL FROM: an MX and an SPF record on mail.example.com
cf dns records create -z example.com --body '{"type":"MX","name":"mail.example.com","content":"feedback-smtp.us-east-1.amazonses.com","priority":10,"ttl":1}'
cf dns records create -z example.com --body '{"type":"TXT","name":"mail.example.com","content":"\"v=spf1 include:amazonses.com ~all\"","ttl":1}'
# DMARC: start at p=none (monitor only)
cf dns records create -z example.com --body '{"type":"TXT","name":"_dmarc.example.com","content":"\"v=DMARC1; p=none;\"","ttl":1}'
Add --dry-run to any of these to see the request first. If the zone has a _dmarc record already, change that record instead of adding a second one: a domain must have one DMARC record only.
The SPF record goes on mail.example.com, not on the apex. The apex SPF record belongs to whoever receives mail for example.com, for example Cloudflare Email Routing. A domain must have one SPF record per name, so never add a second apex SPF record for SES.
4. Check it
With Cloudflare DNS, verification usually takes about two minutes:
aws sesv2 get-email-identity --email-identity example.com --region us-east-1 \
--query '[VerifiedForSendingStatus,DkimAttributes.Status,MailFromAttributes.MailFromDomainStatus]'
# [true, "SUCCESS", "SUCCESS"]
Then send one real email from production and look at its headers: SPF, DKIM and DMARC must all say PASS. The deliverability guide has the one-line command, and the steps after launch: DMARC reports, a stricter DMARC policy, and bounce and complaint notifications.