Article

Records that stop anyone sending mail as your parked domains

A domain that publishes no mail policy can be put in anyone's From: line, and four DNS records close that gap for every domain you own but do not use for mail.

6 min read

The problem

Most companies own more domains than they use: old brand names, typo variants, a product that never launched, a domain kept so that no one else gets it. They sit "parked", which usually means one of three things. The domain has a zone with no records. The domain has a registrar parking page and nothing else. Or, more often than you would think, the domain is delegated to nameservers that no longer hold a zone for it, and every query returns SERVFAIL.

All three look harmless because nothing visible depends on them. The risk is invisible: a receiving mail server deciding whether to trust a message that claims to come from billing@your-old-brand.com looks up that domain's SPF record and its DMARC policy. If there is none, there is no instruction to reject. The receiver falls back to its own heuristics, and a well-crafted phishing message using your name gets a fair hearing. A parked domain without a mail policy is an unclaimed sending identity that still carries your brand.

The third state is the worst. A domain that does not resolve cannot publish a policy, so it cannot tell anyone to reject anything.

The pattern: four records that all say no

A domain that sends and receives no mail should say so explicitly, in four places:

RecordNameValueWhat it tells a receiver
MX@0 .This domain accepts no mail (RFC 7505 "null MX").
TXT (SPF)@v=spf1 -allNo server is authorized to send as this domain.
TXT (DMARC)_dmarcv=DMARC1; p=reject; sp=reject; adkim=s; aspf=s;Reject anything that fails, here and on subdomains.
TXT (DKIM)*._domainkeyv=DKIM1; p=Every signing key for this domain is revoked.

In zone-file form, for any DNS host:

; A parked domain: accepts no mail, sends no mail, signs nothing.
example.com.               3600 IN MX  0 .
example.com.               3600 IN TXT "v=spf1 -all"
_dmarc.example.com.        3600 IN TXT "v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s;"
*._domainkey.example.com.  3600 IN TXT "v=DKIM1; p="

Why each one:

  • Null MX stops mail to the domain cleanly. Without any MX, some senders fall back to the domain's A record and try to deliver there; with no A record either, they queue and retry for days. 0 . is an unambiguous "no", and many receivers also treat a domain that declares it accepts no mail as an unlikely genuine sender.
  • SPF -all, not ~all. The soft fail ~all exists for domains that are still migrating senders and want leniency. A parked domain has no senders to be lenient about.
  • DMARC p=reject is the record that actually instructs the receiver. SPF and DKIM only produce pass or fail; DMARC tells the receiver what to do with a failure. Since nothing is authorized, everything fails, and p=reject says to refuse it.
  • A revoked DKIM wildcard. A DKIM record whose p= is empty is the standard way (RFC 6376) to say "this key is revoked". As a wildcard under _domainkey it answers for every selector, so a receiver checking any signature gets a definite "revoked" answer for a missing key.

About sp=reject

sp is the DMARC policy for subdomains. It is commonly described as essential, as if subdomains of a domain without it inherit nothing. That is not how DMARC works: when sp is absent, the subdomain policy defaults to the value of p (RFC 7489, section 6.3). A receiver checking invoices.example.com that finds no _dmarc.invoices.example.com record falls back to the organizational domain's record and applies sp, or p if sp is missing. So with p=reject, subdomains are already covered. We still write sp=reject: it is explicit, it survives someone later softening p during an unrelated change, and it costs nothing. But p is the setting doing the work, and sp is a second lock behind it.

adkim=s and aspf=s request strict alignment. On a domain that authorizes nothing they change no outcome, and they document intent.

The Terraform version

If your DNS is code, make parked domains one list and one record set, identical by construction, so there is no per-domain reasoning to keep straight. With the Cloudflare provider and a small zone module:

locals {
  parked_domains = toset(["<parked-1>.com", "<parked-2>.com", "<parked-3>.com", "<parked-4>.com"])

  # The whole of a parked domain's DNS: four records that all say "no".
  parked_records = {
    # RFC 7505 null MX: "this domain accepts no mail", stated unambiguously.
    "|MX"  = { type = "MX",  content = ".", ttl = 1, priority = 0 }
    # -all, not ~all. There is no sender to be soft about.
    "|TXT" = { type = "TXT", content = "v=spf1 -all", ttl = 1 }
    # p=reject is the instruction. sp defaults to p (RFC 7489 6.3); sp=reject
    # makes it explicit and survives a later edit to p.
    "_dmarc|TXT" = { type = "TXT", content = "v=DMARC1; p=reject; sp=reject; aspf=s; adkim=s;", ttl = 1 }
    # Empty p= is the RFC 6376 way to say "revoked"; as a wildcard it answers every selector.
    "*._domainkey|TXT" = { type = "TXT", content = "v=DKIM1; p=", ttl = 1 }
  }
}

# No zone settings block: these zones serve no web traffic, and TLS settings
# would imply a web presence that does not exist.
module "parked" {
  source     = "./modules/zone"
  for_each   = local.parked_domains
  name       = each.key
  account_id = local.account_id
  records    = local.parked_records
}

If one of these domains ever grows a real service, it leaves the list and gets its own module block with real SPF includes and DKIM selectors.

Order of operations

The records are the easy part. Getting them served is where parked domains go wrong.

  1. Create the zone first. The zone must exist at the DNS provider before the registrar points at it, or the domain goes dark in between. If your registrar and DNS are managed in separate Terraform states, nothing will enforce this order for you; it is a sequencing rule for a human.
  2. Read the assigned nameservers. Some providers, Cloudflare among them, assign a nameserver pair per zone. Do not assume the pair your other zones use; read it from the zone after creation (expose it as a Terraform output).
  3. Point the registrar's delegation at them.
  4. Verify from outside. Ask the delegated nameserver directly, then a public resolver.

A note on automation. With some DNS providers, creating a zone from CI fails with a 403 even when the API token holds every account-level permission the documentation lists, because zone creation can need rights tied to the account owner, and some of those touch billing. A CI token that can change billing is a worse trade than one manual click per new zone. A workable split: create each zone by hand, once, then adopt it into Terraform with an import block whose plan must show no changes. The records, which change, stay fully in code.

Verify it

A short script checks the four records and the delegation against a public resolver:

#!/usr/bin/env bash
# Check a parked domain's four "no" records against a public resolver.
d="${1:?usage: $0 example.com}"
r="1.1.1.1"
echo "NS:     $(dig +short NS "$d" @"$r" | tr '\n' ' ')"
echo "MX:     $(dig +short MX "$d" @"$r")"                        # expect: 0 .
echo "SPF:    $(dig +short TXT "$d" @"$r" | grep -i 'v=spf1')"      # expect: "v=spf1 -all"
echo "DMARC:  $(dig +short TXT "_dmarc.$d" @"$r")"                 # expect: p=reject
echo "DKIM:   $(dig +short TXT "anyselector._domainkey.$d" @"$r")" # expect: "v=DKIM1; p="
status=$(dig "$d" SOA @"$r" | awk '/status:/{print $6}' | tr -d ',')
echo "Status: $status"                                             # SERVFAIL = broken delegation

If the status is SERVFAIL, the records do not matter yet: the domain is delegated to nameservers that do not serve it. Compare the registrar's nameserver list with what the DNS provider says the zone should use, and query one of the delegated nameservers directly (dig SOA example.com @ns1.provider.net). A known-good domain at the same nameserver is a useful control: if it answers and yours does not, the fault is a missing zone and the resolver is fine.

Traps

  • A registrar parking page protects nothing. It usually means an A record and nothing else. Check for the four records explicitly.
  • Check before you -all a domain that is not really parked. Old brand domains sometimes still send: a forgotten newsletter tool, an invoicing system, a ticket desk. Search your mail logs and your SaaS settings for the domain first; -all with p=reject will stop those messages, which is the point only if they are not wanted.
  • Do not start at p=none here. p=none is the right first step for a domain with real senders whose alignment you are still discovering. A parked domain has none to discover. Going straight to reject is correct.
  • Reporting is optional and has a cost. Adding rua=mailto:... gets you aggregate reports showing who is trying to send as the domain, which can be useful evidence. If the reporting address is on a different domain, that domain must publish an authorization record for it (RFC 7489, section 7.1), and someone has to read the reports. If no one will, leave it out.
  • SPF does not cover subdomains; DMARC does. An SPF record at the apex says nothing about x.example.com. With p=reject (or sp=reject) at the organizational domain, a message from a subdomain fails alignment and is rejected anyway. A wildcard *.example.com TXT "v=spf1 -all" is optional extra cover, but remember that DNS wildcards do not apply to names that already have records.
  • Do not let the zone and the registrar disagree. A stale delegation to nameservers that no longer hold your zone is the most common reason a "parked" domain publishes nothing at all.

Related articles

Running into something like this?

Thirty minutes, no commitment. Bring the problem; we will tell you what we would check first and whether we are the right people for it.

All articles