Short answer: serve customer assets from a mapped domain only after the DNS records, signed-URL access control, and mail-authentication identities are tracked as separate contracts; for property mail, compare SPF, DKIM, and DMARC intent with published values on every release.
The least complex way to keep a property-management mailbox deliverable is to treat SPF, DKIM, and DMARC as one published contract, then continuously compare that contract with the sending systems you intend to run. DNS is the release surface; the ledger is the control surface. Signed links and asset CDNs do not repair an authentication mismatch.
How should customer domains serve assets while DNS records stay auditable?
For a portfolio of apartment communities, I start with one row per customer domain. The row names the envelope sender, the DKIM selector, the policy, and the systems allowed to send. A useful first pass might contain leasing.example with SPF v=spf1 include:mail.example.net -all, selector leasing2026, and DMARC v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.net; adkim=s; aspf=s.
That string is not a recommendation to copy blindly. The include mechanism must identify an authorized sender, and the reporting address needs a mailbox that can actually receive aggregate reports. RFC 7489 defines DMARC's policy and reporting model; RFC 7208 defines SPF evaluation, while RFC 6376 defines DKIM signatures. Keep those standards beside the change request.
The important detail is intent. “The leasing app sends mail” is not enough. Record the exact domain in the visible From header, the return-path domain used for SPF, and the selector that signs each message. Alignment is evaluated between those identities, not between a human label and a DNS dashboard.
How do I publish records without losing the thread?
I use a small TypeScript check in CI before asking a customer to change DNS. It turns the expected records into data, resolves the live values, and reports drift as a reviewable diff. It deliberately does not attempt to mutate DNS.
type Expected = {
domain: string;
spfContains: string;
dkimName: string;
dmarcName: string;
dmarcPolicy: "none" | "quarantine" | "reject";
};
type Resolver = (name: string, type: "TXT") => Promise<string[]>;
export async function checkDns(expected: Expected, resolve: Resolver) {
const [spfValues, dkimValues, dmarcValues] = await Promise.all([
resolve(expected.domain, "TXT"),
resolve(expected.dkimName, "TXT"),
resolve(expected.dmarcName, "TXT"),
]);
const spf = spfValues.find((value) => value.startsWith("v=spf1")) ?? "";
const dkim = dkimValues.join(" ");
const dmarc = dmarcValues.find((value) => value.startsWith("v=DMARC1")) ?? "";
return {
spfAligned: spf.includes(expected.spfContains) && spf.endsWith("-all"),
dkimPresent: dkim.length > 0,
dmarcPolicy: /(?:^|;)\s*p=(none|quarantine|reject)/.exec(dmarc)?.[1] ?? null,
policyMatches: dmarc.includes(`p=${expected.dmarcPolicy}`),
};
}
The check has a narrow purpose. It catches a missing selector and a policy that was weakened during a handoff; it cannot prove that every outbound message is signed correctly. A staging message still needs inspection of its headers, and aggregate reports need a separate parser with retention and access controls.
DNS answers are cached. A low TTL can make a planned cutover easier to observe, but it cannot force resolvers to forget an older answer immediately. I therefore schedule the change, capture the previous TXT values, and leave a rollback record with an owner and an expiry time. That small bit of paperwork has saved more delivery time than another dashboard.
Where does drift actually appear?
The common failure is a split-brain change: an operations team adds a new sender to SPF, while the application keeps signing with an old DKIM selector. Another is an apparently harmless p=none left in place after a trial. Both states can look healthy in a DNS console while DMARC reports show alignment failures.
The trade-off is explicit: stronger enforcement improves spoofing resistance, but a rushed policy change can reject legitimate notices.
Stop.
Property teams also rotate vendors. A resident portal, maintenance scheduler, and accounting system may each send as the same customer domain. SPF has a lookup limit, so combining every provider's include record without counting mechanisms can make otherwise valid mail fail evaluation. DKIM reduces that pressure, but each selector still needs a lifecycle: who owns it, when it expires, and how a compromised key is revoked.
I keep report processing separate from the sending path. Aggregate XML reports are evidence for the ledger, not a synchronous dependency for sending a lease notice. Store normalized results with the report date, source IP, disposition, and aligned identifiers; redact message content because DMARC aggregate reports are not a message archive.
A practical release decision and its limits
The release is ready when three things agree: the intended identities in the ledger, the records visible through multiple public resolvers, and the headers from a real test message. If one disagrees, stop the cutover and fix the contract instead of raising the policy to reject as a signal of confidence.
This method has limits. It is a poor fit for a team that cannot receive DMARC reports, cannot name a DNS owner, or needs immediate revocation at edge scale; that team should keep mail authentication and asset authorization in managed systems with explicit on-call ownership. A ledger also cannot validate a provider's internal signing path, and a signed URL does not prove that a message passed DMARC. Those boundaries are why I keep the two controls separate. The operational cost is real: every additional sender adds a review path, a selector rotation date, and another source of report noise. The benefit is traceability, not a promise of perfect delivery.
For a new customer domain, I publish and observe with p=none, verify that legitimate sources align, then tighten policy in a scheduled change. The exact interval depends on report volume and the customer’s tolerance for false positives; the decision should be based on evidence, not a calendar promise. Keep signed asset URLs and CDN hostnames in their own access-control model. They protect files; SPF, DKIM, and DMARC establish who may represent the mail domain.
My operational checklist is prose by design: name an owner for each selector, record the previous TXT values, test the visible and envelope identities, inspect a delivered message, watch aggregate reports after every sender change, and put a date on every temporary exception. When the ledger and the wire disagree, the wire wins and the ledger gets corrected.
Top comments (0)