PowerShell Tipp: SPF Records abfragen

Die immer mehr verbreite Nutzung von SaaS E-Mail Dienstleistern ala Exchange Online, G-Suite, Amazon SES, SendGrid und anderer ist eine Herausforderung für E-Mail Administratoren. Um zu verhindern das E-Mails an die Kunden im Spam Filter landen oder gar nicht angenommen werden, ist eine einwandfreie Konfiguration von DMARC, DKIM und SPF Pflicht.
Gerade bei SPF ist man schnell dazu geneigt einfach alle include Einträge der Drittanbieter einzupflegen. Das kann aber auch ins Auge gehen, wenn z.B. die maximale Anzahl an DNS Lookups (10) oder die maximale Länge eines TXT DNS Records (255) überschritten wird. Gerade bei include sind die Auswirkungen nicht immer direkt sichtbar, weil viele Dienste selbst ebenfalls include nutzen um die IP Ranges zu organisieren.
Der Überblick über die eingebundenen Dienste kann da schnell verloren gehen.
Um die vorhandenen SPF DNS Records, inklusive aller per include eingetragenen Diensten abzufragen, habe ich die Funktion Resolve-SPFRecord geschrieben.
Funktionsübersicht
Unterstützte SPF Direktiven und Funktionen
- include
- mx
- a
- ip4 und ip6
- redirect
- Warnung bei zu vielen
includeEinträgen
Nicht unterstützt werden
- exp
- Makros
Nutzung
Für die Abfrage der entsprechenden TXT Records im DNS wird nur der Paramater Name benötigt. Hier muss die abzufragende Domäne angegeben werden, das Skript macht den Rest.
Resolve-SPFRecord -Name domainname.tld
Es empfiehlt sich, für bessere Lesbarkeit, das Ergebnis mit Format-Table ausgeben zu lassen.
Resolve-SPFRecord -Name domainname.tld | ft

Alternativer DNS Server
Optional kann noch der Paramater Server genutzt werden. Hiermit ändert man den abzufragenden DNS Server. Das kann z.B. hilfreich sein, wenn man die DNS Änderungen kurz nach der Veröffentlichung direkt am eigenen Root Nameserver testen möchte, oder es z.B. Einschränkungen gibt welchen DNS Server der eigene Client abfragen darf.
Download
Das komplette Skript steht hier zum Download bereit oder kann auch aus dem Code Block kopiert werden.
function Resolve-SPFRecord {
[CmdletBinding()]
param (
# Domain Name
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
Position = 1)]
[string]$Name,
# DNS Server to use
[Parameter(Mandatory = $false,
ValueFromPipelineByPropertyName = $true,
Position = 2)]
[string]$Server = "1.1.1.1",
# If called nested provide a referrer to build valid objects
[Parameter(Mandatory = $false)]
[string]$Referrer
)
begin {
class SPFRecord {
[string] $SPFSourceDomain
[string] $IPAddress
[string] $Referrer
[string] $Qualifier
[bool] $Include
# Constructor: Creates a new SPFRecord object, with a specified IPAddress
SPFRecord ([string] $IPAddress) {
$this.IPAddress = $IPAddress
}
# Constructor: Creates a new SPFRecord object, with a specified IPAddress and DNSName
SPFRecord ([string] $IPAddress, [String] $DNSName) {
$this.IPAddress = $IPAddress
$this.SPFSourceDomain = $DNSName
}
# Constructor: Creates a new SPFRecord object, with a specified IPAddress and DNSName and
SPFRecord ([string] $IPAddress, [String] $DNSName, [String] $Qualifier) {
$this.IPAddress = $IPAddress
$this.SPFSourceDomain = $DNSName
$this.Qualifier = $Qualifier
}
}
}
process {
# Keep track of number of DNS queries
# DNS Lookup Limit = 10
# https://tools.ietf.org/html/rfc7208#section-4.6.4
# Query DNS Record
$DNSRecords = Resolve-DnsName -Server $Server -Name $Name -Type TXT
# Check SPF record
$SPFRecord = $DNSRecords | Where-Object { $_.Strings -match "^v=spf1" }
# Validate SPF record
$SPFCount = ($SPFRecord | Measure-Object).Count
if ( $SPFCount -eq 0) {
# If there is no error show an error
Write-Error "No SPF record found for `"$Name`""
}
elseif ( $SPFCount -ge 2 ) {
# Multiple DNS Records are not allowed
# https://tools.ietf.org/html/rfc7208#section-3.2
Write-Error "There is more than one SPF for domain `"$Name`""
}
else {
# Multiple Strings in a Single DNS Record
# https://tools.ietf.org/html/rfc7208#section-3.3
$SPFString = $SPFRecord.Strings -join ''
# Split the directives at the whitespace
$SPFDirectives = $SPFString -split " "
# Check for a redirect
if ( $SPFDirectives -match "redirect" ) {
$RedirectRecord = $SPFDirectives -match "redirect" -replace "redirect="
Write-Verbose "[REDIRECT]`t$RedirectRecord"
# Follow the include and resolve the include
Resolve-SPFRecord -Name "$RedirectRecord" -Server $Server -Referrer $Name
}
else {
# Extract the qualifier
$Qualifier = switch ( $SPFDirectives -match "^[+-?~]all$" -replace "all" ) {
"+" { "pass" }
"-" { "fail" }
"~" { "softfail" }
"?" { "neutral" }
}
$ReturnValues = foreach ($SPFDirective in $SPFDirectives) {
switch -Regex ($SPFDirective) {
"%[{%-_]" {
Write-Warning "[$_]`tMacros are not supported. For more information, see https://tools.ietf.org/html/rfc7208#section-7"
Continue
}
"^exp:.*$" {
Write-Warning "[$_]`tExplanation is not supported. For more information, see https://tools.ietf.org/html/rfc7208#section-6.2"
Continue
}
'^include:.*$' {
# Follow the include and resolve the include
Resolve-SPFRecord -Name ( $SPFDirective -replace "^include:" ) -Server $Server -Referrer $Name
}
'^ip[46]:.*$' {
Write-Verbose "[IP]`tSPF entry: $SPFDirective"
$SPFObject = [SPFRecord]::New( ($SPFDirective -replace "^ip[46]:"), $Name, $Qualifier)
if ( $PSBoundParameters.ContainsKey('Referrer') ) {
$SPFObject.Referrer = $Referrer
$SPFObject.Include = $true
}
$SPFObject
}
'^a:.*$' {
Write-Verbose "[A]`tSPF entry: $SPFDirective"
$DNSRecords = Resolve-DnsName -Server $Server -Name $Name -Type A
# Check SPF record
foreach ($IPAddress in ($DNSRecords.IPAddress) ) {
$SPFObject = [SPFRecord]::New( $IPAddress, ($SPFDirective -replace "^a:"), $Qualifier)
if ( $PSBoundParameters.ContainsKey('Referrer') ) {
$SPFObject.Referrer = $Referrer
$SPFObject.Include = $true
}
$SPFObject
}
}
'^mx:.*$' {
Write-Verbose "[MX]`tSPF entry: $SPFDirective"
$DNSRecords = Resolve-DnsName -Server $Server -Name $Name -Type MX
foreach ($MXRecords in ($DNSRecords.NameExchange) ) {
# Check SPF record
$DNSRecords = Resolve-DnsName -Server $Server -Name $MXRecords -Type A
foreach ($IPAddress in ($DNSRecords.IPAddress) ) {
$SPFObject = [SPFRecord]::New( $IPAddress, ($SPFDirective -replace "^mx:"), $Qualifier)
if ( $PSBoundParameters.ContainsKey('Referrer') ) {
$SPFObject.Referrer = $Referrer
$SPFObject.Include = $true
}
$SPFObject
}
}
}
Default {
Write-Warning "[$_]`t Unknown directive"
}
}
}
$DNSQuerySum = $ReturnValues | Select-Object -Unique SPFSourceDomain | Measure-Object | Select-Object -ExpandProperty Count
if ( $DNSQuerySum -gt 6) {
Write-Warning "Watch your includes!`nThe maximum number of DNS queries is 10 and you have already $DNSQuerySum.`nCheck https://tools.ietf.org/html/rfc7208#section-4.6.4"
}
if ( $DNSQuerySum -gt 10) {
Write-Error "Too many DNS queries made ($DNSQuerySum).`nMust not exceed 10 DNS queries.`nCheck https://tools.ietf.org/html/rfc7208#section-4.6.4"
}
$ReturnValues
}
}
}
end {
}
}