How to set up SPF, DKIM and DMARC on AWS Route 53
Route 53 has one quirk that catches everyone, and it isn't the console: TXT values must be wrapped in double quotes. AWS states it plainly — "A TXT record contains one or more strings that are enclosed in double quotation marks." The path itself is short: Hosted zones → your zone → Create record → type TXT → Record name → Value → Create records.
The second quirk costs more time than the first: a hosted zone that exists is not a hosted zone that answers. Creating one doesn't make Route 53 authoritative for your domain — your registrar still has to point at the four name servers that zone was assigned. Get that wrong, or edit a second hosted zone with the same name, and every record below is correct and invisible.
These three records let receiving mail servers check that a message claiming to be from you really is from you. They do not guarantee your email reaches the inbox — your content and your sending reputation matter at least as much — but without them, anyone can send email in your name.
What we don't do: we never touch your DNS zone. This guide tells you what to type and where. You're the one who saves it, in your own account, with your own credentials.
Before anything else: is this hosted zone the one the world reads?
Route 53 makes this failure easier than any other DNS host, because it lets you create as many hosted zones as you like with the same name. AWS documents both halves of the trap.
The mechanism: "You can create more than one hosted zone that has the same name and add different records to each hosted zone. Route 53 assigns four name servers to every hosted zone, and the name servers are different for each of them. When you update your registrar's name server records, be careful to use the Route 53 name servers for the correct hosted zone… Route 53 never returns values for records in other hosted zones that have the same name."
The consequence, in AWS's own troubleshooting page: "If you add, change, or delete records in one hosted zone but your domain registration is using the name servers for another hosted zone, Route 53 responses to DNS queries won't reflect your changes."
How to check, before you type anything:
- In the navigation pane, choose Hosted zones.
- On the Hosted zones page, choose the radio button — not the name — for your zone, then choose View details.
- Choose Hosted zone details and note the four servers listed under Name servers.
- Compare those four with the name servers set at your registrar. If they don't match, you're about to edit a zone nobody queries.
That "radio button, not the name" instruction is AWS's own wording, on two separate pages. It's an odd detail to document, which usually means enough people got it wrong.
The records to copy and paste
1. SPF — who is allowed to send as you
| Field | Value |
|---|---|
| Record name | (leave empty for your root domain) |
| Record type | TXT |
| Value | "v=spf1 include:_spf.google.com ~all" |
| TTL | 300 |
What you must replace: the include:, and quite possibly the whole approach. The line above is Google Workspace's published value, used here as an example. Use whatever your mail provider gives you:
| Your email service | What goes in your SPF |
|---|---|
| Google Workspace | include:_spf.google.com |
| Microsoft 365 | include:spf.protection.outlook.com |
| Amazon SES, default setup | (nothing on your root domain — see the box below) |
| Amazon WorkMail | the value in WorkMail's domain setup screen |
| SendGrid, Mailchimp, Klaviyo, HubSpot | the value shown in their interface |
Keep the quotes. AWS's example values in its own documentation are written "v=spf1 ip4:192.168.0.1/16 -all", quotes included, and its WorkMail guide tells you to copy the value "including the quotation marks."
If several services send on your behalf, they all go in one single line, inside one pair of quotes:
"v=spf1 include:_spf.google.com include:spf.example-crm.com ~all"
⚠️ One SPF record per domain. Always. Two
v=spf1lines don't mean double protection, they mean no SPF at all. When you add a sending tool, you edit the line you already have.
⚠️ Don't confuse "another string" with "another value". In the Value box, several strings on the same line separated by spaces are one record split into chunks. A value on a new row is a separate TXT record — AWS: "To enter more than one TXT value, enter one value per row." That's how people accidentally end up with two SPF records.
If you send through Amazon SES, don't expect to see it in your root SPF — and don't add it. AWS documents that messages sent through SES use a subdomain of
amazonses.comas the default MAIL FROM domain, and that "SPF authentication successfully validates these messages… Therefore, in SES, SPF is implicitly set up for you." If you configure a custom MAIL FROM domain, the SPF record goes on that subdomain, not your root:"v=spf1 include:amazonses.com ~all", alongside an MX record10 feedback-smtp.region.amazonses.com. A scan of your root domain will never see SES in your SPF, and that is normal.
2. DKIM — the signature on your messages
Route 53 doesn't generate your DKIM key. Whoever sends your email does.
If you send through Amazon SES with Easy DKIM, you get three CNAME records, not a TXT:
| Field | Value |
|---|---|
| Record name | the token shown in the SES console, followed by ._domainkey |
| Record type | CNAME |
| Value | TOKEN.dkim.amazonses.com (or a Region-specific variant) |
| TTL | 300 |
What you must replace: all three tokens — they're generated for your identity. AWS describes the format as "the DKIM token followed by a hosted zone domain (for example, token.dkim.amazonses.com or token.a31d.dkim.us-west-2.amazonses.com)". Copy the three rows from the Publish DNS records table in the SES console.
SES can write them for you. If your domain is in Route 53 on the same account, SES offers a Publish DNS records to Route53 checkbox, enabled by default, at the moment you create the identity. For a custom MAIL FROM domain the equivalent button is worded Publish Records Using Route 53. If you take that route, you don't type anything — but do go and look at what landed in the zone.
If you use BYODKIM, Google Workspace, Microsoft 365 or anything else, you're publishing a TXT record whose value is a long public key, and that's where Route 53's second quirk lives:
🔴 A 2048-bit DKIM key will not fit in one string. AWS: "A single string can include up to 255 characters." And the fix, in AWS's words: "If you need to enter a value longer than 255 characters, break the value into strings of 255 characters or fewer, and enclose each string in double quotation marks. In the console, list all the strings on the same line:
"String 1" "String 2" "String 3"." The total value can go up to 4,000 characters. The receiving server reassembles the chunks — this isn't a workaround, it's how DNS carries long text.
⚠️ Never trust a tutorial that hands you a ready-made Microsoft 365 DKIM value. Since May 2025, Microsoft uses a new CNAME format for custom domains added after that date, with a partition character assigned dynamically — it is genuinely impossible to guess.
3. DMARC — the instruction you give to inboxes
| Field | Value |
|---|---|
| Record name | _dmarc |
| Record type | TXT |
| Value | "v=DMARC1; p=none; rua=mailto:YOU@YOUR-DOMAIN.com" |
| TTL | 300 |
What you must replace: YOU@YOUR-DOMAIN.com with an address you actually read. That's where your reports land, and a DMARC record without a rua= is a report you'll never see.
Type _dmarc on its own, not _dmarc.yourdomain.com. Route 53 completes the name with the hosted zone: AWS's own instruction for a subdomain is "if the name of the hosted zone is example.com and you want to use acme.example.com… enter acme."
Why p=none first. It means "watch and tell me". Nobody gets blocked, nothing changes for your recipients, and you start receiving a list of everything sending in your name — including the tools you'd forgotten about. You watch for two to four weeks, confirm every legitimate sender is recognised, and only then tighten one notch to p=quarantine, then p=reject.
AWS's own DMARC example starts at
p=quarantine. SES documents"v=DMARC1;p=quarantine;rua=mailto:my_dmarc_report@example.com"while also describing the three-step rollout that begins withp=none. Start atnone— you'll reachquarantinein a few weeks knowing what you're doing rather than guessing.
Credit where it's due:
pct=appears nowhere in AWS's documentation. That tag was removed from the DMARC standard by RFC 9989, published in May 2026, and servers following the new standard ignore it. AWS never recommended it — unlike Google, which still does.
One SES-specific detail worth knowing: "In order to achieve SPF alignment with SES, the domain's DMARC policy must not specify a strict SPF policy (
aspf=s)." If you're tempted by strict alignment, that's the trade-off.
The click path
- Open the Route 53 console and choose Hosted zones in the navigation pane.
- Choose the name of the hosted zone you want to add records to. (Note: to read the zone's name servers instead, you select the radio button — see above.)
- Choose Create record.
- If you land on a Choose routing policy page, choose Switch to quick create. If you're on Quick create record and want the guided version, the link is Switch to wizard. Either works; quick create is faster for these three records.
- Fill in Record name — leave it empty for the root domain, or type
_dmarc. - Pick TXT from Record type. The console describes it as "TXT — Text — A text record. Enclose text in quotation marks."
- Paste your value with its double quotes into Value.
- Set TTL (seconds) —
300is a good value while you're setting things up. It's not the console's default; it's AWS's own recommendation for records you're actively changing ("initially specify a shorter value, such as 300 seconds, and increase the value after you confirm that the new settings are correct"). Raise it once everything verifies. - Leave Routing policy on Simple routing.
- Choose Create records.
The Record name field defaults to your zone name. AWS: "The default value is the name of the hosted zone." And for the root domain specifically: "If you're creating a record that has the same name as the hosted zone, don't enter a value (for example, an @ symbol) in the Name field." That's the opposite of most registrars, where
@is exactly what you type. On Route 53, leave it blank.
On TTL, and this is worth two minutes: AWS recommends that "if you're changing settings for a domain or subdomain that's already in use… initially specify a shorter value, such as 300 seconds, and increase the value after you confirm that the new settings are correct." And a warning that saves a lot of pointless waiting — "Changing the TTL now won't make your change take effect faster. DNS resolvers already have the value cached, and they won't get the new setting until the amount of time that was specified by the old setting passes." Lower the TTL before the change, not after.
The 3 mistakes we see most often on Route 53
1. Publishing a TXT record without its quotes
You paste v=spf1 include:_spf.google.com ~all into the Value box, save, and the record exists. Whether it works depends on something AWS documents as a requirement but doesn't spell out at the moment you type.
How you spot it: the record shows up in the console but an outside lookup returns nothing usable, or your provider's verification never completes. Every AWS example value for TXT is written with quotes — yours should be too.
The fix: wrap the whole value in one pair of double quotes. If the value itself contains a quote, AWS wants it escaped with a backslash: \".
2. Editing a hosted zone that isn't the live one
You add all three records, everything saves, and nothing changes — not for your mail provider, not on any scan. Almost always, there are two hosted zones with the same name, and your registrar points at the other one's name servers.
This is Route 53's signature failure. AWS lets you create duplicate zones on purpose, gives each a different set of four name servers, and warns that it "never returns values for records in other hosted zones that have the same name."
How you spot it: list your hosted zones and look for the same domain twice. Then compare that zone's four name servers with what your registrar has.
The fix: either point your registrar at the correct zone's name servers, or move your records into the zone that's actually delegated. Then delete the spare — leaving it there guarantees someone repeats the mistake in six months.
⚠️ Changing name servers is not a free action. AWS: "If you change the name servers that are associated with your domain registration, your domain will be unavailable on the internet for up to 2 days." If your zone is already live and correct, fix the records, not the delegation.
3. Adding include:amazonses.com to your root domain because a blog said so
SES is aligned through DKIM and through its MAIL FROM subdomain, not through your root SPF. AWS never asks you to put include:amazonses.com on your root domain — for the default setup it says SPF "is implicitly set up for you", and for a custom MAIL FROM domain the record belongs on that subdomain.
Adding it to your root anyway usually means creating a second v=spf1 line, which breaks SPF for everything. And if you merge it into your existing line instead, you've spent one of your ten DNS lookups on a record that authorises nothing you send.
How you spot it: your root SPF mentions amazonses.com, and either a scan reports multiple SPF records, or you're close to the ten-lookup limit for no benefit.
The fix: take it out of the root line. If you want SPF alignment for SES, configure a custom MAIL FROM subdomain and publish the record there, as AWS documents.
⚠️ And don't mix up two opposite traps. On Route 53 the Record name field wants the prefix only — leave it blank for the root, type
_dmarcfor DMARC — and typing the full hostname gives you_dmarc.yourdomain.com.yourdomain.com; the fix is to shorten it. At other hosts (OVH, Gandi) the symmetrical trap sits on a CNAME target, where the fix is to add a trailing dot. Same look, opposite fixes.
How long before it takes effect
- Inside Route 53: fast. "Changes generally propagate to all Route 53 name servers within 60 seconds." That's propagation within AWS, not worldwide visibility.
- On the internet: governed by the TTL that was in place before your change. At 300 seconds, resolvers refresh within five minutes.
- If you changed name servers: a different story entirely. "Domain registrars typically use a TTL of 24 to 48 hours for name servers", so a delegation change "can take up to two days for the change to take effect."
- For your provider's "Verify" button: SES notes it can take up to 72 hours for DNS changes to propagate before an identity verifies. Wait, then wait again.
- For DMARC reports: the first ones arrive 24 to 72 hours after you publish, then roughly once a day.
Next steps
- Set up SendGrid domain authentication on AWS Route 53
- How to set up SPF, DKIM and DMARC on Cloudflare
- How to set up SPF, DKIM and DMARC on GoDaddy
- How to move your DMARC policy to p=reject safely
- My emails go to spam even though SPF, DKIM and DMARC pass