DMARC DNS Record Implementation Guide

Comprehensive technical documentation of all DMARC DNS record fields, validation constraints, and implementation requirements per RFC 7489.

The DMARC standard (RFC 7489, March 2015) defines 11 official fields published in DNS TXT records at _dmarc.domain.com. The upcoming DMARCbis (expected 2025) will deprecate some tags while maintaining v=DMARC1 compatibility.

DMARC Record Syntax (ABNF Grammar)

DMARC records use Augmented Backus-Naur Form (ABNF) per RFC 5234. The complete grammar specification:

; Complete ABNF Grammar for DMARC Records (RFC 7489) dmarc-record = dmarc-version dmarc-sep dmarc-request *( dmarc-sep dmarc-tag ) dmarc-version = "v" *WSP "=" *WSP %s"DMARC1" dmarc-request = "p" *WSP "=" *WSP dmarc-preq dmarc-preq = "none" / "quarantine" / "reject" dmarc-sep = *WSP ";" *WSP dmarc-tag = dmarc-atag / dmarc-ftag / dmarc-otag / dmarc-ptag / dmarc-rtag / dmarc-stag ; Alignment tags dmarc-atag = "adkim" *WSP "=" *WSP ("r" / "s") dmarc-asfpt = "aspf" *WSP "=" *WSP ("r" / "s") ; Percentage tag (being replaced by t=y/n in DMARCbis) dmarc-ptag = "pct" *WSP "=" *WSP 1*3DIGIT ; Reporting tags dmarc-rtag = "rua" *WSP "=" *WSP dmarc-uri / "ruf" *WSP "=" *WSP dmarc-uri dmarc-uri = URI [ "!" 1*DIGIT [ "k" / "m" / "g" / "t" ] ] *( "," URI [ "!" 1*DIGIT [ "k" / "m" / "g" / "t" ] ] ) ; Subdomain policy dmarc-stag = "sp" *WSP "=" *WSP dmarc-preq ; Failure reporting options dmarc-ftag = "fo" *WSP "=" *WSP dmarc-fopt dmarc-fopt = "0" / "1" / "d" / "s" / (dmarc-fv *(";" dmarc-fv)) dmarc-fv = "0" / "1" / "d" / "s" ; Tags deprecated in DMARCbis dmarc-otag = "ri" *WSP "=" *WSP 1*DIGIT / "rf" *WSP "=" *WSP "afrf"

Critical Syntax Rules

  • Mandatory ordering: v=DMARC1 MUST be first tag, p= MUST be second
  • Case sensitivity: Tag names lowercase, DMARC1 uppercase, policy values lowercase
  • Whitespace handling: Optional around = and ; separators
  • String concatenation: Multiple DNS TXT strings automatically concatenated
  • Maximum length: Individual TXT strings limited to 255 characters

Mandatory separators: Semicolon (;) between tags, equals sign (=) between tag and value, comma (,) for URI lists.

Mandatory Fields

v (Version)

  • Type: Fixed string
  • Value: "DMARC1" (case-sensitive)
  • Position: First mandatory tag
  • Validation: Exact match required

p (Policy)

  • Type: Enumeration
  • Values: "none" | "quarantine" | "reject"
  • Position: Second mandatory tag
  • Function: Processing policy for primary domain

Optional Control Fields (Detailed Specifications)

sp (Subdomain Policy)

; Syntax sp-tag = "sp" *WSP "=" *WSP ( "none" / "quarantine" / "reject" ) ; Validation Rules - MUST use exact lowercase string values - If omitted, subdomains inherit parent domain policy (p=) - Applies to all subdomains (*.example.com) - Does not affect parent domain processing

Subdomain Inheritance Logic: When sp= is absent, mail.example.com inherits the p= policy from example.com. When sp= is present, it overrides this inheritance for all subdomains.

adkim (DKIM Alignment Mode)

; Syntax adkim-tag = "adkim" *WSP "=" *WSP ( "r" / "s" ) ; Alignment Behavior Relaxed (r): DKIM d= organizational domain matches From: domain - From: [email protected] - DKIM d=example.com → PASS (organizational match) Strict (s): DKIM d= domain exactly matches From: domain - From: [email protected] - DKIM d=example.com → FAIL (exact match required) - DKIM d=mail.example.com → PASS (exact match)

aspf (SPF Alignment Mode)

; Syntax aspf-tag = "aspf" *WSP "=" *WSP ( "r" / "s" ) ; Alignment Behavior Relaxed (r): SPF-validated domain organizationally matches From: - From: [email protected] - SPF validates example.com → PASS Strict (s): SPF-validated domain exactly matches From: - From: [email protected] - SPF validates example.com → FAIL - SPF validates mail.example.com → PASS

Organizational Domain Determination

DMARC alignment currently uses the Public Suffix List (PSL) to determine organizational domains. DMARCbis will replace this with DNS Tree Walk algorithm:

# Examples of organizational domain matching example.com → example.com (base domain) mail.example.com → example.com (subdomain of base) api.mail.example.com → example.com (nested subdomain) # Special cases with PSL example.co.uk → example.co.uk (co.uk is public suffix) mail.example.co.uk → example.co.uk (organizational match) # Edge cases example.github.io → example.github.io (github.io is public suffix) api.example.github.io → example.github.io (organizational match) # DMARCbis Tree Walk (upcoming) # Traverses DNS hierarchy up to 7 labels to find DMARC records # Replaces external PSL dependency with pure DNS approach

Reporting Fields

rua (Aggregate Report URI) - Detailed Specification

; Complete ABNF for RUA rua-tag = "rua" *WSP "=" *WSP rua-uri-list rua-uri-list = rua-uri *( "," rua-uri ) rua-uri = "mailto:" addr-spec [ "!" max-size ] max-size = 1*DIGIT [ size-unit ] size-unit = "k" / "m" / "g" / "t" ; kilo/mega/giga/tera ; Size Limits (bytes) k = 1024 m = 1024^2 (1,048,576) g = 1024^3 (1,073,741,824) t = 1024^4 (1,099,511,627,776)

External Domain Validation: When rua points to external domains, receiving systems MUST verify authorization:

# DMARC record at example.com v=DMARC1; p=none; rua=mailto:[email protected] # Required authorization at external.com example.com._report._dmarc.external.com. IN TXT "v=DMARC1" # Without this record, reports to external.com are discarded # Format: {policy-domain}._report._dmarc.{external-domain}

ruf (Forensic Report URI) - Technical Details

; RUF uses identical syntax to RUA ruf-tag = "ruf" *WSP "=" *WSP rua-uri-list ; Privacy Concerns (Why rarely implemented) - Contains full message headers - May include message body content - Privacy regulations (GDPR) compliance issues - High volume potential (per-message vs daily aggregate) - Major providers (Gmail, Yahoo, Microsoft) no longer send ; Recommended: Use RUA only for most implementations

fo (Failure Options) - Complete Specification

; ABNF Grammar fo-tag = "fo" *WSP "=" *WSP fo-option-list fo-option-list = fo-option *( ":" fo-option ) fo-option = "0" / "1" / "d" / "s" ; Detailed Semantics 0 - Generate report if both SPF and DKIM produce something other than "pass" (default behavior) 1 - Generate report if either SPF or DKIM produces something other than "pass" d - Generate report if DKIM evaluation fails to produce "pass" s - Generate report if SPF evaluation fails to produce "pass" ; Combination Examples fo=0 - Both must fail (most restrictive) fo=1 - Either fails (most reports) fo=1:d - Either fails OR DKIM specifically fails fo=d:s - DKIM fails OR SPF fails (equivalent to fo=1)

Failure Option Processing Logic

# Pseudo-code for fo= processing def should_generate_ruf_report(spf_result, dkim_result, fo_options): spf_fail = spf_result != "pass" dkim_fail = dkim_result != "pass" for option in fo_options: if option == "0" and spf_fail and dkim_fail: return True elif option == "1" and (spf_fail or dkim_fail): return True elif option == "d" and dkim_fail: return True elif option == "s" and spf_fail: return True return False

Critical Validation Constraints

Mandatory tag ordering: The most common error is incorrect ordering. v=DMARC1 must be first, followed by p=.

Strict syntax validation: Enumerated values are case-sensitive (except DMARC1). A value "Reject" instead of "reject" invalidates the entire record.

DNS TXT constraints: Maximum length of 255 characters per string. Long records require automatic concatenation of multiple strings.

Cross-domain verification: For rua and ruf URIs pointing to external domains, a DNS record _report._dmarc.external-domain is mandatory to authorize report reception.

DMARC Record Examples and Validation

Progressive Deployment Examples

Phase 1: Initial Monitoring

# Minimal monitoring record v=DMARC1; p=none; rua=mailto:[email protected] # Enhanced monitoring with forensic reports v=DMARC1; p=none; rua=mailto:[email protected]; ruf=mailto:[email protected]; fo=1

Phase 2: Gradual Enforcement

# Quarantine with percentage rollout (operational issues at intermediate values) v=DMARC1; p=quarantine; pct=25; rua=mailto:[email protected] # DMARCbis approach: use testing flag instead v=DMARC1; p=quarantine; t=y; rua=mailto:[email protected] # t=y means testing mode (like pct=0), t=n means enforce (like pct=100)

Phase 3: Full Protection

# Strict alignment with subdomain protection v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s; rua=mailto:[email protected] # Production enterprise configuration v=DMARC1; p=reject; sp=quarantine; rua=mailto:[email protected]!50m,mailto:[email protected]

Complex Configuration Examples

Multi-Domain Reporting

# Company using external DMARC service v=DMARC1; p=quarantine; rua=mailto:[email protected],mailto:[email protected]!100m # Required authorization at external service # DNS: example.com._report._dmarc.dmarcanalyzer.com. IN TXT "v=DMARC1"

High-Volume Domain Configuration

# Large organization with size limits and backup reporting v=DMARC1; p=reject; sp=none; rua=mailto:[email protected]!500m,mailto:[email protected]!1g; adkim=r; aspf=r

Edge Cases and Special Configurations

Subdomain-Only Protection

# Protect subdomains while leaving parent domain open v=DMARC1; p=none; sp=reject; rua=mailto:[email protected] # Use case: Parent domain doesn't send email, # but subdomains (mail.example.com) do

DNS TXT Record Splitting (Long Records)

# Single logical record split across multiple TXT strings _dmarc.example.com. IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]!10m" "mailto:backup-reports-with-another-very-long-email@monitoring.external-service.com!50m; adkim=s; aspf=s; sp=quarantine" # DNS automatically concatenates: "v=DMARC1; p=reject; rua=mailto:..."

Common Invalid Records (Anti-Patterns)

# ❌ INVALID: Wrong tag order p=reject; v=DMARC1; rua=mailto:[email protected] # ❌ INVALID: Case sensitivity error v=dmarc1; p=REJECT; rua=mailto:[email protected] # ❌ INVALID: Missing mandatory p= tag v=DMARC1; rua=mailto:[email protected] # ❌ INVALID: Invalid policy value v=DMARC1; p=block; rua=mailto:[email protected] # ❌ INVALID: Malformed URI v=DMARC1; p=none; [email protected] ; missing mailto: # ❌ INVALID: Percentage out of range v=DMARC1; p=quarantine; pct=150; rua=mailto:[email protected]

DMARC Record Generation and Validation Algorithm

Standard Generation Algorithm

# DMARC Record Generation (Pseudocode) def generate_dmarc_record(policy, options={}): # Step 1: Initialize with mandatory tags record = "v=DMARC1" record += "; p=" + policy # policy must be none|quarantine|reject # Step 2: Add optional tags in recommended order if 'sp' in options: record += "; sp=" + options['sp'] if 'adkim' in options: record += "; adkim=" + options['adkim'] # r|s if 'aspf' in options: record += "; aspf=" + options['aspf'] # r|s if 'pct' in options and options['pct'] != 100: record += "; pct=" + str(options['pct']) # 0-100 if 'rua' in options: record += "; rua=" + format_uri_list(options['rua']) if 'ruf' in options: record += "; ruf=" + format_uri_list(options['ruf']) if 'fo' in options: record += "; fo=" + ":".join(options['fo']) # 0,1,d,s # Step 3: Validate total length and split if needed return split_dns_record_if_needed(record) def format_uri_list(uri_list): formatted = [] for uri_info in uri_list: uri = uri_info['address'] if 'size_limit' in uri_info: uri += "!" + str(uri_info['size_limit']) + uri_info['unit'] formatted.append(uri) return ",".join(formatted)

Validation Algorithm

# DMARC Record Validation (Pseudocode) def validate_dmarc_record(record_string): errors = [] # Parse tag-value pairs tags = parse_dmarc_tags(record_string) # Rule 1: First tag must be v=DMARC1 if not tags or tags[0] != ('v', 'DMARC1'): errors.append("First tag must be v=DMARC1") # Rule 2: Second tag must be p= if len(tags) < 2 or not tags[1][0] == 'p': errors.append("Second tag must be p=") # Rule 3: Validate policy values if tags[1][1] not in ['none', 'quarantine', 'reject']: errors.append("Invalid policy value: " + tags[1][1]) # Rule 4: Validate all tag syntax for tag_name, tag_value in tags: if tag_name == 'sp' and tag_value not in ['none', 'quarantine', 'reject']: errors.append("Invalid sp value: " + tag_value) elif tag_name == 'adkim' and tag_value not in ['r', 's']: errors.append("Invalid adkim value: " + tag_value) elif tag_name == 'aspf' and tag_value not in ['r', 's']: errors.append("Invalid aspf value: " + tag_value) elif tag_name == 'pct': try: pct_val = int(tag_value) if not 0 <= pct_val <= 100: errors.append("pct must be 0-100") except ValueError: errors.append("pct must be integer") elif tag_name in ['rua', 'ruf']: validate_uri_list(tag_value, errors) return errors def validate_uri_list(uri_string, errors): for uri in uri_string.split(','): if not uri.strip().startswith('mailto:'): errors.append(f"URI must use mailto: scheme: {uri}") # Additional URI validation...

DNS Record Length Handling

# Handle DNS TXT record 255-character limit def split_dns_record_if_needed(record): if len(record) <= 255: return record # Split at word boundaries, preferring after semicolons parts = [] current_part = "" for char in record: if len(current_part) + 1 > 255 and char in [';', ' ']: parts.append(current_part) current_part = "" current_part += char if current_part: parts.append(current_part) # Return as multiple DNS TXT strings return parts # Example DNS zone file format for long records _dmarc.example.com. IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]" "additional tags and values continue here"

IANA DMARC Tag Registry

The official IANA registry defines 11 standardized tags:

  • RFC 7489 tags: adkim, aspf, fo, p, pct, rf, ri, rua, ruf, sp, v
  • DMARCbis changes: Removes pct, rf, ri; adds t (testing flag), np (non-existent policy), psd (public suffix domain)
  • Note: Records remain v=DMARC1 for backward compatibility

Deprecated Fields (DMARCbis)

The following fields are being deprecated in DMARCbis and should be avoided in new implementations:

pct (Percentage)

  • Type: Integer
  • Range: 0-100
  • Default: 100
  • Status: Being replaced by t=y/n flag in DMARCbis
  • Issues: Only pct=0 and pct=100 work reliably; intermediate values inconsistent
  • Migration: Use t=y for testing (like pct=0), omit for enforcement

ri (Report Interval)

  • Type: Integer (seconds)
  • Default: 86400 (24h)
  • Status: Removed in DMARCbis (rarely honored by receivers)
  • Reality: Most receivers send daily regardless of ri value

rf (Report Format)

  • Type: Enumeration
  • Value: "afrf" (only supported value)
  • Status: Removed in DMARCbis (no other formats ever implemented)

DMARC Testing and Validation Tools

Command-Line Validation

# Check DMARC DNS record dig TXT _dmarc.example.com +short # Test from multiple DNS servers for server in 8.8.8.8 1.1.1.1 208.67.222.222; do echo "Testing $server:" dig @$server TXT _dmarc.example.com +short done # Validate DMARC syntax with Python python3 -c " import re record = 'v=DMARC1; p=reject; rua=mailto:[email protected]' if re.match(r'^v=DMARC1;.*p=(none|quarantine|reject)', record): print('Valid DMARC record') else: print('Invalid DMARC record') "

Comprehensive DMARC Testing Script

#!/bin/bash # Comprehensive DMARC record validation script DOMAIN="$1" if [ -z "$DOMAIN" ]; then echo "Usage: $0 domain.com" exit 1 fi echo "🔍 Testing DMARC for $DOMAIN..." # Check if DMARC record exists DMARC_RECORD=$(dig TXT "_dmarc.$DOMAIN" +short | tr -d '"' | tr -d ' ') if [ -z "$DMARC_RECORD" ]; then echo "❌ No DMARC record found at _dmarc.$DOMAIN" exit 1 fi echo "📧 DMARC Record: $DMARC_RECORD" # Validate version tag (must be first) if [[ $DMARC_RECORD =~ ^v=DMARC1 ]]; then echo "✅ Valid version tag (v=DMARC1)" else echo "❌ Invalid or missing version tag" fi # Check policy tag (must be second) if [[ $DMARC_RECORD =~ ;p=(none|quarantine|reject) ]]; then POLICY=$(echo "$DMARC_RECORD" | grep -o 'p=[^;]*' | cut -d'=' -f2) echo "✅ Valid policy: $POLICY" else echo "❌ Invalid or missing policy tag" fi # Check for reporting addresses if [[ $DMARC_RECORD =~ rua=mailto: ]]; then RUA=$(echo "$DMARC_RECORD" | grep -o 'rua=[^;]*' | cut -d'=' -f2-) echo "📊 Aggregate reporting: $RUA" else echo "⚠️ No aggregate reporting configured" fi # Validate alignment modes if [[ $DMARC_RECORD =~ adkim=([rs]) ]]; then ADKIM="${BASH_REMATCH[1]}" echo "🔒 DKIM alignment: $([ "$ADKIM" = "s" ] && echo "strict" || echo "relaxed")" fi if [[ $DMARC_RECORD =~ aspf=([rs]) ]]; then ASPF="${BASH_REMATCH[1]}" echo "🔒 SPF alignment: $([ "$ASPF" = "s" ] && echo "strict" || echo "relaxed")" fi # Check subdomain policy if [[ $DMARC_RECORD =~ sp=(none|quarantine|reject) ]]; then SP=$(echo "$DMARC_RECORD" | grep -o 'sp=[^;]*' | cut -d'=' -f2) echo "🏢 Subdomain policy: $SP" else echo "🏢 Subdomain policy: inherits from main policy ($POLICY)" fi echo "✅ DMARC validation complete for $DOMAIN"

Technical Implementation: This specification provides the complete foundation for implementing RFC 7489 compliant DMARC record generators and validators. All syntax rules, validation constraints, and edge cases are comprehensively documented.

Common Implementation Errors: Wrong tag ordering, case sensitivity mistakes, invalid policy values, malformed URIs, missing external domain authorization, and exceeding DNS record length limits. Always validate against this specification.

Additional Resources