Auditing and Enforcing SPF, DKIM, and DMARC Across All Microsoft 365 Tenant Domains with PowerShell

In almost every enterprise Microsoft 365 tenant, the primary corporate domain has well-maintained SPF, DKIM, and DMARC records. However, when security teams perform a full-tenant domain inventory, they almost always uncover a blind spot:

  • Secondary, acquired, or parked domains that were verified in Entra ID years ago but never had p=reject DMARC or -all SPF configured.
  • Domains with DKIM disabled in Exchange Online (Enabled : False in Get-DkimSigningConfig), even when CNAME records were created.
  • **The default MOERA (*.onmicrosoft.com) domain, which attackers can abuse if DMARC is missing.
With strict sender enforcement across Google, Yahoo, and Microsoft Consumer/Enterprise mail servers, any domain lacking aligned SPF, 2048-bit DKIM, and an enforced DMARC policy is both a deliverability risk and an active domain-spoofing liability.

In this guide, we break down the baseline authentication posture required for both active mail-sending domains and parked domains, and provide a complete PowerShell compliance auditor that inspects every verified domain in your tenant and exports a graded CSV scorecard.


The Three-Pillar Authentication Baseline

Microsoft 365 Multi-Domain Email Authentication Matrix
Pillar 1: SPF (RFC 7208)
Sender Policy Framework
  • Active: v=spf1 include:spf.protection.outlook.com -all
  • Parked: v=spf1 -all
  • Max 10 DNS lookups
Pillar 2: DKIM (RFC 6376)
DomainKeys Identified Mail
  • Publish selector1 & selector2 CNAMEs
  • Enable signing via New/Set-DkimSigningConfig
  • Use 2048-bit RSA keys
Pillar 3: DMARC (RFC 7489)
Domain-based Conformance
  • Enforce v=DMARC1; p=reject;
  • Include pct=100 & rua=mailto:...
  • Cover *.onmicrosoft.com

Don't Forget Parked & Non-Sending Domains!

If a domain is registered in your tenant for brand protection or Entra ID UPN routing but
never sends email, leaving DNS empty allows anyone to spoof it. Lock down every non-sending domain with explicit null records:
  • Null MX Record: . 0 (MX 0 .)
  • Null SPF Record: v=spf1 -all
  • Strict DMARC Reject:** v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s;

Production PowerShell Script: Tenant-Wide SPF, DKIM & DMARC Compliance Auditor

This script enumerates every verified domain in your Exchange Online tenant (Get-AcceptedDomain), cross-references its Exchange Online DKIM signing state (Get-DkimSigningConfig), queries public DNS for SPF, DKIM CNAMEs, and _dmarc TXT records, and assigns a clear compliance verdict.

Prerequisites

POWERSHELL
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -UserPrincipalName admin@lab365.in

Full Audit Script (Audit-M365DomainEmailAuth.ps1)

POWERSHELL
<#
.SYNOPSIS
    Audits SPF, DKIM, and DMARC posture across all verified domains in a Microsoft 365 tenant.
.DESCRIPTION
    Enumerates all accepted domains in Exchange Online, queries Get-DkimSigningConfig,
    resolves public SPF, DKIM CNAME, and DMARC TXT records, and exports a compliance CSV report.
.NOTES
    Author: Sunil Chauhan (https://www.lab365.in)
    Requirements: ExchangeOnlineManagement, Windows PowerShell 5.1 or PowerShell 7
#>

[CmdletBinding()]
param (
    [Parameter(Mandatory = $false)]
    [string]$DnsServer = "1.1.1.1",

    [Parameter(Mandatory = $false)]
    [string]$ExportPath = ".\M365_EmailAuth_Compliance_Report_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
)

Write-Host "==========================================================" -ForegroundColor Cyan
Write-Host "  Microsoft 365: Multi-Domain SPF / DKIM / DMARC Auditor  " -ForegroundColor Cyan
Write-Host "==========================================================" -ForegroundColor Cyan

# Retrieve all accepted domains and DKIM signing configurations
$domains = Get-AcceptedDomain
$dkimConfigs = Get-DkimSigningConfig

# Index DKIM configs by domain name
$dkimIndex = @{}
foreach ($dk in $dkimConfigs) {
    $dkimIndex[$dk.Domain] = $dk
}

$results = [System.Collections.Generic.List[PSCustomObject]]::new()

foreach ($d in $domains) {
    $domain = $d.DomainName
    Write-Host "[*] Auditing domain: $domain..." -ForegroundColor Yellow

    # 1. Check SPF Record
    $txtRecords = Resolve-DnsName -Name $domain -Type TXT -Server $DnsServer -ErrorAction SilentlyContinue |
                  Where-Object { $_.Type -eq 'TXT' }
    $spfRecords = @($txtRecords | ForEach-Object { $_.Strings -join '' } | Where-Object { $_ -match "^v=spf1\b" })

    $spfRecord = if ($spfRecords.Count -eq 1) {
        $spfRecords[0]
    } elseif ($spfRecords.Count -gt 1) {
        "MULTIPLE SPF RECORDS ($($spfRecords.Count)) - RFC Violation"
    } else {
        "Missing"
    }

    $spfStatus = if ($spfRecords.Count -gt 1) {
        "FAIL: Multiple SPF records"
    } elseif ($spfRecord -match "-all\b") {
        "PASS: HardFail (-all)"
    } elseif ($spfRecord -match "~all\b") {
        "WARN: SoftFail (~all)"
    } elseif ($spfRecord -match "\?all|\+all") {
        "FAIL: Permissive (?all/+all)"
    } else {
        "FAIL: Missing SPF"
    }

    # 2. Check DKIM Configuration & CNAMEs
    $dkimObj = $dkimIndex[$domain]
    $dkimEnabled = if ($dkimObj) { [bool]$dkimObj.Enabled } else { $false }
    $dkimKeySize = if ($dkimObj) { $dkimObj.Selector1KeySize } else { "N/A" }

    $sel1 = Resolve-DnsName -Name "selector1._domainkey.$domain" -Type CNAME -Server $DnsServer -ErrorAction SilentlyContinue |
            Where-Object { $_.Type -eq 'CNAME' }
    $sel2 = Resolve-DnsName -Name "selector2._domainkey.$domain" -Type CNAME -Server $DnsServer -ErrorAction SilentlyContinue |
            Where-Object { $_.Type -eq 'CNAME' }

    $dkimStatus = if ($dkimEnabled -and $dkimKeySize -ge 2048) {
        "PASS: Enabled ($dkimKeySize-bit)"
    } elseif ($dkimEnabled) {
        "WARN: Enabled ($dkimKeySize-bit - Rotate to 2048)"
    } elseif ($sel1 -and $sel2) {
        "ACTION NEEDED: CNAMEs published, run Enable-DkimSigningConfig"
    } else {
        "FAIL: Disabled & Missing CNAMEs"
    }

    # 3. Check DMARC Record (_dmarc.<domain>)
    $dmarcTxts = Resolve-DnsName -Name "_dmarc.$domain" -Type TXT -Server $DnsServer -ErrorAction SilentlyContinue |
                 Where-Object { $_.Type -eq 'TXT' }
    $dmarcList = @($dmarcTxts | ForEach-Object { $_.Strings -join '' } | Where-Object { $_ -match "^v=DMARC1\b" })
    $dmarcRecord = if ($dmarcList.Count -ge 1) { $dmarcList[0] } else { "Missing" }

    $dmarcPolicy = if ($dmarcRecord -match "(?i)\bp=([a-z]+)") { $matches[1].ToLower() } else { "none/missing" }

    $dmarcStatus = switch ($dmarcPolicy) {
        "reject"     { "PASS: Enforce (p=reject)" }
        "quarantine" { "PASS: Enforce (p=quarantine)" }
        "none"       { "WARN: Monitoring Only (p=none)" }
        default      { "FAIL: Missing DMARC" }
    }

    # Overall Compliance Score
    $isCompliant = ($spfStatus -like "PASS*") -and ($dkimStatus -like "PASS*") -and ($dmarcStatus -like "PASS*")

    $results.Add([PSCustomObject]@{
        Domain         = $domain
        IsDefault      = $d.Default
        SPFStatus      = $spfStatus
        DKIMStatus     = $dkimStatus
        DMARCStatus    = $dmarcStatus
        FullyCompliant = $isCompliant
        SPFRecord      = $spfRecord
        DMARCRecord    = $dmarcRecord
    })
}

Write-Host ""
$results | Format-Table Domain, SPFStatus, DKIMStatus, DMARCStatus, FullyCompliant -AutoSize
$results | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding utf8
Write-Host "[+] Compliance scorecard exported to: $ExportPath" -ForegroundColor Green

Remediating Common Findings

1. Enabling DKIM Signing or Rotating 1024-bit Keys to 2048-bit

If the script flags a domain where CNAMEs are published but DKIM signing is disabled—or if an older domain is still using a 1024-bit key—remediate it immediately in Exchange Online PowerShell:
POWERSHELL
$domain = "lab365.in"

# Initialize DKIM config if not yet created, and print required CNAME values
New-DkimSigningConfig -DomainName $domain -KeySize 2048 -Enabled $false
Get-DkimSigningConfig -Identity $domain | Format-List Selector1CNAME, Selector2CNAME

# Once CNAMEs are published in DNS, enable DKIM signing
Set-DkimSigningConfig -Identity $domain -Enabled $true

# If already enabled with a legacy 1024-bit key, rotate to 2048-bit
Rotate-DkimSigningConfig -Identity $domain -KeySize 2048

2. Securing Your Default .onmicrosoft.com Domain

Even if you never send external business emails from <tenant>.onmicrosoft.com, system notifications or SharePoint invitations may occasionally originate from it, and external actors can attempt to spoof it:
  • SPF & DKIM: Automatically managed by Microsoft for .onmicrosoft.com.
  • DMARC: Add a _dmarc TXT record for your .onmicrosoft.com domain directly inside the Microsoft 365 Admin Center (Settings > Domains > select <tenant>.onmicrosoft.com > DNS records > + Add record*):
- Type: TXT - Name: _dmarc - TXT Value: v=DMARC1; p=reject; pct=100;

Summary Checklist

  • [ ] Run Audit-M365DomainEmailAuth.ps1 quarterly across all verified tenant domains.
  • [ ] Upgrade all 1024-bit DKIM keys to 2048-bit using Rotate-DkimSigningConfig -KeySize 2048.
  • [ ] Transition DMARC policies from p=none to p=quarantine and finally p=reject.
  • [ ] Publish _dmarc TXT records on your .onmicrosoft.com domain inside the M365 Admin Center.
  • [ ] Apply v=spf1 -all and v=DMARC1; p=reject; to all parked or non-sending domains.

Related reading: Configuring Trusted ARC Sealers in Exchange Online (Set-ArcConfig) · Enabling Inbound SMTP DANE with DNSSEC in Exchange Online