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=rejectDMARC or-allSPF configured. - Domains with DKIM disabled in Exchange Online (
Enabled : FalseinGet-DkimSigningConfig), even when CNAME records were created. - **The default MOERA (
*.onmicrosoft.com) domain, which attackers can abuse if DMARC is missing.
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
- Active:
v=spf1 include:spf.protection.outlook.com -all - Parked:
v=spf1 -all - Max 10 DNS lookups
- Publish
selector1&selector2CNAMEs - Enable signing via
New/Set-DkimSigningConfig - Use 2048-bit RSA keys
- 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:. 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
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -UserPrincipalName admin@lab365.in
Full Audit Script (Audit-M365DomainEmailAuth.ps1)
<#
.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:$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
_dmarcTXT record for your.onmicrosoft.comdomain directly inside the Microsoft 365 Admin Center (Settings > Domains > select<tenant>.onmicrosoft.com> DNS records > + Add record*):
TXT
- Name: _dmarc
- TXT Value: v=DMARC1; p=reject; pct=100;
Summary Checklist
- [ ] Run
Audit-M365DomainEmailAuth.ps1quarterly across all verified tenant domains. - [ ] Upgrade all
1024-bitDKIM keys to2048-bitusingRotate-DkimSigningConfig -KeySize 2048. - [ ] Transition DMARC policies from
p=nonetop=quarantineand finallyp=reject. - [ ] Publish
_dmarcTXT records on your.onmicrosoft.comdomain inside the M365 Admin Center. - [ ] Apply
v=spf1 -allandv=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