DEV Community

CloudveilElenor12
CloudveilElenor12

Posted on

Lowering TTL Early Versus Publishing SPF DKIM and DMARC Records on Email Cutover Day

A support desk migration has one clock you don't control: the TTL on the records resolvers have already cached. It starts ticking when you publish, not when you switch senders. That single property decides how an SPF, DKIM and DMARC setup should be sequenced, and it separates a quiet migration from a day of email deliverability complaints arriving from customers whose password resets never showed up. Pick the pre-staged schedule whenever the domain already carries live ticket mail — drop the TTL one full old-TTL period ahead, publish all three records, verify them from a resolver you don't operate, and only then point the help desk at the new sender. The cutover-day publish is defensible on exactly one kind of domain: one that has never sent anything, where no cached answer exists to wait out.

Everything below follows from that clock.

The propagation clock starts before the cutover

Lowering a TTL is not a fast operation, which is the part people skip. A resolver that fetched the TXT set at 09:00 under a TTL of 86400 s keeps serving that answer until 09:00 the following day no matter what you publish at 09:05, so the new value only governs behaviour after the old one has drained. A 300 s TTL is worth having at the switch only if it was published at least one full old-TTL period earlier. Twenty-four hours of patience buys five minutes of agility, and that exchange rate is fixed by RFC 1035 caching semantics rather than by any provider.

Negative answers cache as well, and that is the trap specific to help-desk migrations. Under RFC 2308 a resolver asked for selector7._domainkey.support.example.com before that name exists caches the non-existence for the lesser of the SOA MINIMUM field and the SOA record's own TTL. Domain verification probes from the incoming mail platform are precisely the traffic that manufactures such a cached negative: you attach the domain, the platform checks immediately, the lookup comes back empty, and now every resolver on that path holds "no such name" for as long as the zone's SOA instructs. Publishing a name before anything asks for it costs nothing. Explaining the resulting delay to a support manager costs a great deal more.

So two records deserve to exist long before they mean anything. Put _dmarc.support.example.com in place carrying v=DMARC1; p=none; rua=..., and publish the incoming DKIM selector with its public key. Neither one changes the handling of a single message until the sender actually starts signing, and both remove a negative cache you would otherwise be waiting out on the day.

How should one support domain cover SPF, DKIM and DMARC before the mail cutover?

Split the question by what each record asserts, because the three overlap far less than the shared acronym block suggests.

SPF (RFC 7208) authorises hosts to put your domain in the SMTP envelope sender. It lives in a TXT record at the name itself, it must appear exactly once — two v=spf1 TXT records at one name is a permanent error rather than a merge — and evaluation is capped at ten mechanisms that trigger DNS queries. Support stacks cross that cap quietly, because an include: for the ticketing platform, another for the survey sender and a third for the transactional relay all draw on the same budget of ten.

DKIM (RFC 6376) publishes a key at selector._domainkey.domain so a receiver can verify a signature over selected headers and the body. The selector is the migration primitive. Nothing prevents two selectors from being live simultaneously, and that overlap is what lets the outgoing and incoming senders both authenticate through a switchover window of your choosing.

DMARC (RFC 7489) is the only one of the three that carries policy. It requires that SPF or DKIM not merely pass but align with the domain in the From header, and it states what a receiver should do when neither aligns. A record at _dmarc.example.com covers subdomains unless an sp= tag overrides it, which matters when the help desk sends from support.example.com while the parent domain runs marketing.

One aligned mechanism is enough. Plan for DKIM to be the one that survives, since DKIM signatures generally survive forwarding and mailing lists while SPF alignment often does not.

Verification then has to come from outside your own cache, because your recursive resolver is the last place that will tell you the truth about a name you just changed. A DNS-over-HTTPS query (RFC 8484) is a cheap way to ask a resolver you don't operate, and it drops straight into a pre-cutover health check:

DOH=https://doh.example.net/dns-query
DOMAIN=support.example.com
SELECTOR=selector7

for name in "$DOMAIN" "_dmarc.$DOMAIN" "$SELECTOR._domainkey.$DOMAIN"; do
  curl -sS --get "$DOH" \
    -H 'accept: application/dns-json' \
    --data-urlencode "name=$name" \
    --data-urlencode "type=TXT" \
    -o "/tmp/check-$name.json" -w "$name %{http_code}\n"
done
Enter fullscreen mode Exit fullscreen mode

Run it against two or three independent resolvers, not one. A single agreeing answer tells you a record resolves somewhere; it says nothing about the resolver a customer's mail server happens to sit behind.

Comparing a pre-staged TTL drop with a cutover-day publish

The two schedules differ in which resource they spend. One spends calendar days to make the switch reversible in minutes; the other spends nothing up front and accepts that the first bad hour is unbounded by anything you can edit.

Sequencing Lead time to budget Rollback speed at the switch Where it stops being the right call
Pre-staged TTL drop One full old-TTL period, plus several days of p=none reports Minutes, bounded by the lowered TTL Domains you don't control end to end, where the customer's IT team publishes on its own calendar
Cutover-day publish None Hours to a day, bounded by whatever TTL was already in the zone Any domain already carrying live ticket mail, because the first misses land on real customers

The catch is that the pre-staged plan trades calendar time for blast radius, and calendar time is the resource a support migration usually has least of. When a contract ends on the 31st, "wait out an 86400 s TTL first" is not always an available sentence. Two partial escapes exist. You can lower the TTL as a standalone change weeks before anyone has agreed on the sending platform, since a low TTL is harmless on its own and costs only a few extra queries. Or you can accept a dual-send window in which both senders sign with their own selectors and both are authorised in SPF, which converts a hard cutover into a drain.

Neither escape helps on a customer-controlled domain. If the person publishing the record works for someone else and answers a ticket queue of their own, the honest schedule is the one their change process supports, and the design question becomes how long you are willing to run two senders rather than how fast DNS can move.

Retention math for DMARC aggregate reports

This is where the plan meets the observability bill, and where most teams overpay for evidence they never read.

Aggregate reports arrive at whatever address the rua tag names, one report per reporting receiver per policy domain per interval, with the interval defaulting to ri=86400. The file count is therefore small and predictable: receivers multiplied by domains multiplied by retention days. A hundred support domains under a dozen reporting receivers is on the order of a thousand XML documents a day, which is nothing in object storage and does not deserve a database.

The cardinality problem lives inside the XML. Each report is a set of rows keyed by source IP, and a single mailing list or forwarding service will scatter dozens of source addresses across a week. Index source IP as a metric label and the series count grows with the internet rather than with your fleet. I'd keep the raw XML in cheap object storage for the window in which a delivery dispute can plausibly arrive, and derive from it exactly one low-cardinality counter set: reporting receiver, policy applied, SPF alignment, DKIM alignment, selector. That set is bounded by your own configuration, so it stays affordable to keep for a year while the raw documents expire on a much shorter clock.

Sampling deserves the same scrutiny. The pct tag applies the requested policy to a fraction of messages, so pct=10 also cuts by ten the rate at which a rare unauthorised source shows up in your evidence. A week of observation becomes ten weeks of observation. That's the honest cost of a cautious enforcement ramp, and it is worth stating out loud before someone concludes from four quiet days that a domain is ready for p=reject.

Failure reports are a different retention class entirely. The ruf stream carries message-level detail, including headers and sometimes content belonging to your customers, so it belongs in the shortest window your policy allows and behind tighter access control than the aggregate stream. Many receivers never send it at all.

The limitation of keeping less is real, and I won't pretend otherwise. If you retain counters and drop raw reports after a short window, a dispute that surfaces months later leaves you able to say that alignment held on a given day without being able to show which source produced the exception. Where a regulated retention duty applies, set that duty first and let the storage plan follow. Your mileage may vary with how often your support organisation actually gets asked.

A rollout order that survives a rollback

The sequence below is ordered so that every step is reversible until the last one, and so that nothing waits on a cache it created itself.

  1. Lower the TTL on the affected names, then wait one full old-TTL period. Nothing else in the plan is valid before this drains.
  2. Publish the incoming DKIM selector's public key while the outgoing selector stays live.
  3. Add the incoming sender to SPF, and count the DNS-querying mechanisms before you commit — flattening or removing a retired include: is easier now than during an incident.
  4. Publish _dmarc with p=none and a rua address you can actually read, then collect several reporting intervals.
  5. Switch the help desk to the new sender, verify alignment from outside, and only afterwards retire the old selector and the old include:.
  6. Raise the policy from p=none toward p=quarantine and p=reject on the evidence, not on the calendar.

Config-as-code tooling such as OctoDNS or DNSControl keeps the record set reviewable in a pull request, which matters more than it sounds: the most frequent way an SPF record goes wrong is a hand edit that drops one include: while adding another. Whatever writes the zone, the provider's DNS API has to accept an explicit TTL per record, and it has to be idempotent on repeat — if it applies a zone default silently, or appends a second v=spf1 TXT instead of replacing the first, the entire pre-staged schedule reverts to guesswork.

ZONE=support.example.com

curl -sS -X PUT "https://dns-api.example.net/zones/${ZONE}/records/_dmarc/TXT" \
  -H "authorization: Bearer ${DNS_API_TOKEN}" \
  -H 'content-type: application/json' \
  -d '{
        "ttl": 300,
        "value": "v=DMARC1; p=none; rua=mailto:dmarc-reports@support.example.com; ri=86400"
      }' \
  -w '\n%{http_code}\n'
Enter fullscreen mode Exit fullscreen mode

One last boundary. If the sending domain belongs to your customer rather than to you, none of this is a DNS problem — it is a change-management problem wearing a DNS costume, and the schedule that matters is theirs. Stick with a long dual-send window in that case, keep p=none until their records are stable, and spend the saved engineering time on the report pipeline instead. The three records are easy. The waiting is the design.

Sources

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.