Back to blog
AWSBest PracticesCloud SecurityNetworkingOperations & Compliance

Route 53 Hosted Zone Missing SPF Record: Stop Email Spoofing

Learn why a missing SPF record on a Route 53 hosted zone enables email spoofing, and how to fix and prevent it with CLI, Terraform, and policy-as-code.

TL;DR

This check flags Route 53 public hosted zones that have no SPF TXT record, which lets anyone send email that appears to come from your domain. Fix it by publishing a TXT record listing your authorized mail senders, for example v=spf1 include:_spf.google.com -all.

If your domain sends email, receiving mail servers want a way to confirm that the sending server is actually allowed to use your domain. SPF is the oldest and simplest of those mechanisms. When a Route 53 hosted zone has no SPF record, you lose that first line of defense and make it trivially easy for someone to forge email from your domain.

This check, route53_nospf, inspects each public hosted zone in Route 53 and reports any zone that has no SPF TXT record at the apex. Below we cover what it means, why it matters, and exactly how to fix and prevent it.


What this check detects

The check queries your AWS Route 53 public hosted zones and looks for a TXT record at the zone apex (the bare domain, like example.com) that begins with v=spf1. If no such record exists, the zone is flagged.

SPF, short for Sender Policy Framework, is a DNS TXT record that lists the IP addresses and hosts authorized to send email on behalf of your domain. A receiving mail server checks the SPF record against the sending server's IP and decides whether to accept, flag, or reject the message.

Note: SPF was historically published using a dedicated DNS record type (type 99). That record type was deprecated by RFC 7208 in 2014. Today SPF must be published as a TXT record. If you find old SPF-type records, treat them as legacy and migrate to TXT.

The check only applies to public hosted zones. Private zones used for internal name resolution do not handle inbound email from the public internet, so SPF is not relevant there.


Why it matters

Without an SPF record, receiving servers have no published rule about who is allowed to send mail from your domain. That has direct, practical consequences.

Email spoofing and phishing

An attacker can send email with a From: address at your domain, and many receiving servers will accept it because nothing tells them to do otherwise. This is the foundation of business email compromise and phishing campaigns. A forged message from [email protected] asking a customer to update payment details is far more convincing than one from a random address.

Deliverability damage

It is not only an attacker problem. Major providers like Google and Microsoft increasingly require SPF (and DKIM and DMARC) for bulk senders. A missing SPF record means your legitimate email is more likely to land in spam folders or be rejected outright, which quietly erodes the reach of your transactional and marketing mail.

DMARC cannot work without it

DMARC, the policy layer that tells receivers what to do with mail that fails authentication, relies on SPF or DKIM to pass with alignment. If you have no SPF record, your DMARC posture is weaker and harder to enforce. SPF is a prerequisite, not an optional add-on.

Warning: Even a domain that never sends email should publish SPF. A "no senders" record (v=spf1 -all) tells the world that this domain should never appear as an email sender, which protects parked or marketing-only domains from being abused for spoofing.


How to fix it

The fix is to publish a TXT record at your zone apex containing a valid SPF policy. The exact value depends on who sends mail for your domain.

Step 1: Identify your senders

List every service that sends email using your domain. Common examples:

  • Google Workspace: include:_spf.google.com
  • Microsoft 365: include:spf.protection.outlook.com
  • Amazon SES: include:amazonses.com
  • SendGrid: include:sendgrid.net
  • Mailchimp: include:servers.mcsv.net

Combine them into a single record. If Google Workspace is your only sender, your SPF record looks like this:

v=spf1 include:_spf.google.com -all

The -all at the end is a hard fail: anything not listed should be rejected. Use ~all (soft fail) only while testing, then tighten to -all.

Warning: SPF allows a maximum of 10 DNS lookups when evaluating a record. Each include, a, mx, ptr, and redirect mechanism can trigger lookups. Exceed 10 and your record returns permerror, which causes legitimate mail to fail. Keep the include list lean and flatten where necessary.

Step 2: Add the record in the Route 53 console

  1. Open the Route 53 console and select Hosted zones.
  2. Choose the public hosted zone for your domain.
  3. Click Create record.
  4. Leave the record name blank to target the apex.
  5. Set the record type to TXT.
  6. Enter the value, wrapped in quotes: "v=spf1 include:_spf.google.com -all"
  7. Set a TTL (300 seconds is fine while testing, raise to 3600 once stable) and save.

Step 3: Add the record with the AWS CLI

First find your hosted zone ID:

aws route53 list-hosted-zones-by-name \
  --dns-name example.com \
  --query "HostedZones[0].Id" --output text

Then create a change batch file, spf-record.json:

{
  "Comment": "Add SPF record",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "example.com",
        "Type": "TXT",
        "TTL": 300,
        "ResourceRecords": [
          { "Value": "\"v=spf1 include:_spf.google.com -all\"" }
        ]
      }
    }
  ]
}

Danger: If your domain already has a TXT record at the apex (for example a domain verification token), using UPSERT on the TXT name will overwrite all existing TXT values. TXT records share a name, so you must include every value you want to keep in a single change. Run a list-resource-record-sets first and merge existing values into the change batch.

Apply the change, replacing the zone ID with yours:

aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890ABC \
  --change-batch file://spf-record.json

Step 4: Define it as code with Terraform

If you manage DNS through infrastructure as code, the record belongs in your repo, not in a one-off CLI run:

resource "aws_route53_record" "spf" {
  zone_id = aws_route53_zone.example.zone_id
  name    = "example.com"
  type    = "TXT"
  ttl     = 3600
  records = ["v=spf1 include:_spf.google.com -all"]
}

Step 5: Verify

After DNS propagates, confirm the record resolves:

dig +short TXT example.com
# expect: "v=spf1 include:_spf.google.com -all"

Tip: Test your record against a real receiver before flipping to -all. Send a message to a Gmail account, open the original, and check the Authentication-Results header for spf=pass. That confirms your senders are covered before you start hard-failing mail.


How to prevent it from happening again

Adding one record by hand fixes one domain. The goal is to make a missing SPF record impossible to ship in the first place.

Make DNS records part of your IaC

Every hosted zone you create through Terraform, CloudFormation, or Pulumi should include its email authentication records in the same module. Bake an SPF record into your zone module so a new domain is never created without one. If a domain sends no mail, default it to v=spf1 -all.

Gate it in CI/CD with policy-as-code

Use a policy engine like OPA or Checkov to fail a pull request that defines a Route 53 public zone without an apex SPF record. A simple Conftest policy against a Terraform plan can assert that any aws_route53_zone has a matching TXT record beginning with v=spf1.

package main

deny[msg] {
  zone := input.resource.aws_route53_zone[name]
  not has_spf_record(name)
  msg := sprintf("Hosted zone '%s' is missing an SPF TXT record", [name])
}

has_spf_record(zone_name) {
  rec := input.resource.aws_route53_record[_]
  rec.type == "TXT"
  startswith(rec.records[_], "v=spf1")
}

Continuously scan live state

IaC checks only catch resources you manage in code. Console-created zones and manual changes slip through. Run a recurring scan against your actual AWS account so drift is caught regardless of how the zone was created. This is exactly what the Lensix route53_nospf check does on every account it monitors.

Tip: Pair the SPF check with DKIM and DMARC checks. SPF alone is a partial defense. A monitoring dashboard that surfaces all three together gives you a true picture of each domain's email authentication posture.


Best practices

  • One SPF record per domain. Publishing two SPF TXT records is invalid and causes a permerror. Merge all senders into a single record.
  • Use -all in production. A soft fail (~all) is for testing. A hard fail tells receivers to reject unauthorized senders, which is the whole point.
  • Stay under 10 DNS lookups. Audit your includes periodically. Drop senders you no longer use and flatten heavy includes if you approach the limit.
  • Protect parked and non-sending domains. Apply v=spf1 -all to every domain you own that does not send mail.
  • Layer SPF, DKIM, and DMARC. SPF authenticates the sending path, DKIM signs the message, and DMARC ties them together with a policy and reporting. Treat them as a set.
  • Review when senders change. Onboarding a new email tool means updating SPF. Make it a checklist item so legitimate mail does not start failing.

SPF is one TXT record, but its absence opens the door to spoofing and quietly hurts your deliverability. Publish a tight record, enforce it in code, and scan continuously so no domain ever goes live without one.