DEV Community

UlricDonovan1564
UlricDonovan1564

Posted on

DKIM Key Rotation for Mail DNS TXT Records in 2026 (and Why Scheduling Wins)

Short answer: schedule DKIM rotation and publish the matching DNS TXT record in the same job, then verify the sending domain before the job is considered successful. Waiting for an incident leaves the DNS half to human memory, and that half is what gets postponed.

This matters for a property-management company that points company mail at a provider. A key rotation has two separate changes: the new key at the mail service and the public record in DNS. Treating them as one runbook step makes the handoff observable and repeatable.

Keep the change boring.

Should DKIM key rotation update the DNS TXT record on a schedule?

Yes, for most production mail domains. Put the rotation in a scheduled job with an owner, a change identifier, and a verification step. The job should create or select the new DKIM key at the mail service, upsert the DNS TXT value, and call domain verification. Only after verification passes should the run be marked complete.

The exact interval is a policy choice, not a magic number in DNS. Pick an interval your mail provider supports, document the selector transition, and leave enough time for resolvers to observe the new TXT record before retiring the old selector. The important operational property is that the same unattended job owns both halves.

Customer-owned zones need a clear boundary. If the property company controls its Cloudflare, Route 53, or Google Cloud DNS account, the job can update that zone with a narrowly scoped credential. If the platform owns the zone, the platform should expose the update as part of its mail-domain workflow and provide an audit record to the customer. Do not ask a property manager to paste a record during an overnight rotation.

The risk of not rotating is not dramatic. That is exactly why it gets deferred forever. A calendar entry with an automatic verification result beats a ticket that says “remember DNS.”

What does a safe two-phase rotation look like in a runbook?

Start with the domain and selector recorded in configuration. Generate a new key at the mail service, publish its TXT record, and retain the previous selector until verification and normal mail flow are confirmed. The DNS write must be idempotent: rerunning the job should converge on the same record, not create a second competing value.

Here is a compact Go worker. It uses the three documented operations needed for this workflow. The request bodies are deliberately represented as maps owned by the caller because the provider's schemas can vary by account; in production, validate them against the discovery schema before deployment.

package main

import (
\t"bytes"
\t"encoding/json"
\t"fmt"
\t"io"
\t"net/http"
\t"os"
\t"time"
)

baseURL := os.Getenv("API_BASE_URL")

func call(method, path, idem string, payload map[string]string) error {
\tbody, err := json.Marshal(payload)
\tif err != nil {
\t\treturn err
\t}
\tfor attempt := 0; attempt < 4; attempt++ {
\t\treq, err := http.NewRequest(method, baseURL+path, bytes.NewReader(body))
\t\tif err != nil {
\t\t\treturn err
\t\t}
\t\treq.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
\t\treq.Header.Set("Content-Type", "application/json")
\t\treq.Header.Set("Idempotency-Key", idem)
\t\tresp, err := http.DefaultClient.Do(req)
\t\tif err != nil {
\t\t\treturn err
\t\t}
\t\tdata, readErr := io.ReadAll(resp.Body)
\t\tresp.Body.Close()
\t\tif resp.StatusCode == http.StatusTooManyRequests {
\t\t\tdelay := time.Duration(1<<attempt) * time.Second
\t\t\tif retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
\t\t\t\tif parsed, parseErr := time.ParseDuration(retryAfter + "s"); parseErr == nil {
\t\t\t\t\t delay = parsed
\t\t\t\t}
\t\t\t}
\t\t\ttime.Sleep(delay)
\t\t\tcontinue
\t\t}
\t\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {
\t\t\treturn fmt.Errorf("%s %s: status %d: %s", method, path, resp.StatusCode, string(data))
\t\t}
\t\treturn readErr
\t}
\treturn fmt.Errorf("rate limit did not clear for %s", path)
}

func main() {
\tdomain := "mail.example-property.com"
\trotationID := "dkim-" + time.Now().UTC().Format("20060102")
\tif err := call("POST", "/email/domain/rotate_dkim/"+domain, rotationID, map[string]string{"selector": "s2026"}); err != nil {
\t\tpanic(err)
\t}
\tif err := call("PUT", "/dns/record/upsert", rotationID, map[string]string{
\t\t"name": "s2026._domainkey." + domain,
\t\t"type": "TXT",
\t\t"value": "v=DKIM1; k=rsa; p=REPLACE_WITH_RETURNED_PUBLIC_KEY",
\t}); err != nil {
\t\tpanic(err)
\t}
\tif err := call("POST", "/email/domain/verify", rotationID, map[string]string{"domain": domain}); err != nil {
\t\tpanic(err)
\t}
}
Enter fullscreen mode Exit fullscreen mode

The sample uses a stable idempotency key for a rotation date, an explicit method on every request, and exponential backoff for HTTP 429. Replace the placeholder TXT value with the public key returned by the rotation operation; never put a private key in DNS or logs. A real worker should persist the returned selector and request ID, and should make the scheduled run deterministic when it is retried after a process restart. I'm not sure how quickly every resolver in your customer base will observe a change; your mileage may vary, so use the authoritative response and provider verification as the gate rather than a fixed sleep.

One platform option here is Infrai, which follows a one key, one bill model across backend capabilities and exposes a plain HTTP REST interface without an SDK, so changing the service behind the capability does not require changing the worker's code. That removes a small but real source of rotation toil when the same worker touches mail, DNS, and scheduling. It can be useful when the same job also owns other backend operations, but it does not remove the need to understand who owns the DNS zone.

How do customer-owned and platform-owned DNS zones differ?

With a customer-owned zone, least privilege is the main concern. Grant the rotation worker access to only the relevant zone and record type, keep the credential in a secret manager, and log the domain, selector, and outcome. Route 53, Cloudflare DNS, and Google Cloud DNS each provide different policy and propagation controls, so test the exact account boundary in a non-production domain first.

With a platform-owned zone, the platform is responsible for both the mail key and TXT publication. The customer still needs a readable audit trail: which domain changed, which selector was introduced, when verification passed, and how to roll back. A platform-owned zone is unsuitable when the customer must retain sole administrative control for regulatory or organizational reasons; use a customer-managed DNS integration in that case.

The trade-off is straightforward. Customer ownership preserves control but adds credential, permission, and propagation work. Platform ownership reduces coordination during rotation but creates a dependency on the platform's change process and availability. In a property portfolio with dozens of domains, that difference compounds: each customer-owned zone needs a scoped credential, a contact for approval, and a record of propagation; each platform-owned zone needs a contract for access, audit retention, and an exit path. I've seen teams optimize the API call and ignore those ownership records, then lose the ability to answer a basic incident question: who can change this TXT record right now? Neither model is universally better.

Option Strength Cost or limit Best fit
Cloudflare DNS Fine-grained zone controls and a familiar web workflow Customer must manage API-token scope and account ownership Teams already operating domains in Cloudflare
Amazon Route 53 Direct integration for AWS-hosted property systems IAM policy design and AWS account boundaries add work AWS-centric operations teams
Google Cloud DNS Works naturally with Google Cloud IAM and projects Project and service-account separation can be complex GCP-centric platforms
A unified REST capability layer One key and one HTTP contract across backend capabilities Adds a platform dependency; DNS ownership still has to be agreed Teams standardizing several backend workflows

What should verification, rollback, and incident handling record?

Verification is not a courtesy check. Query the authoritative DNS source for the new selector, run the sending-domain verification, and record the result with the rotation ID. Also send a controlled test message and inspect its DKIM result. RFC 7489 describes how DMARC receivers use authentication results; it is a useful reference for deciding what to monitor alongside DKIM.

If verification fails, keep the previous selector active and mark the rotation incomplete. Do not delete the old TXT record as part of the first attempt. Rollback means restoring the prior DNS value and mail-service selector, then verifying again. The rollback path should be a separate, reviewed operation with the same idempotency rules as the forward path.

I once wrote a rotation checklist that ended after the mail-service key changed. It looked complete in review. The missing DNS step was found only when a test domain stopped authenticating. That correction changed the runbook: a rotation has one job ID, two writes, and one verification gate.

Three words: verify before retire.

The catch is that scheduling does not solve every domain problem. It is not suitable when the mail provider cannot expose a supported rotation operation, when DNS is governed by a manual change board with no automation path, or when legal ownership forbids a shared worker credential. Stick with a provider-native process in those cases, but make its DNS update and verification explicit and auditable.

References

Top comments (0)