PowerShell DNS Record Audit: Export All Zone Records to CSV

Why DNS Hygiene Matters
DNS zones accumulate records for years without cleanup. Decommissioned servers leave orphaned A records that cause name resolution failures when new machines reuse the same IP. CNAME chains grow circular. MX records point at servers that were retired in a previous migration. In large organisations, a DNS zone can hold thousands of records and nobody has a complete picture of what is current and what is stale. Exporting all records to a structured CSV on a regular schedule enables change tracking, stale record identification, and the kind of before/after comparison that makes DNS migrations manageable rather than terrifying.
Quick Answer
Use Get-DnsServerZone to enumerate all zones, Get-DnsServerResourceRecord per zone to retrieve all records, normalize the type-specific data into a flat table, and export with Export-Csv. Compare today’s export against a previous baseline using Compare-Object to surface changes.
Getting All Zones with Get-DnsServerZone
Get-DnsServerZone lists every zone hosted on the DNS server. Filter to primary and secondary zones to exclude the built-in root hints and cache zones that are not useful for auditing. The -ComputerName parameter lets you query remote DNS servers without an interactive session, provided the DnsServer module and appropriate firewall rules are in place.
Import-Module DnsServer -ErrorAction Stop
$dnsServer = "DC01"
$zones = Get-DnsServerZone -ComputerName $dnsServer |
Where-Object {
$_.ZoneType -in @("Primary","Secondary") -and
-not $_.IsAutoCreated -and
$_.ZoneName -ne "TrustAnchors"
}
Write-Host "Auditable zones found: $($zones.Count)"
$zones | Select-Object ZoneName, ZoneType, IsDsIntegrated, IsReverseLookupZone |
Format-Table -AutoSize
Exporting Records per Zone
Get-DnsServerResourceRecord retrieves all resource records for a named zone. For large zones, always specify -ZoneName rather than querying without a filter — some zones contain tens of thousands of records and an unfiltered query can be slow and memory-intensive. Loop over the zone list and collect all records into a single array for unified processing.
$allRecords = foreach ($zone in $zones) {
Write-Verbose "Querying zone: $($zone.ZoneName)"
try {
Get-DnsServerResourceRecord -ZoneName $zone.ZoneName `
-ComputerName $dnsServer -ErrorAction Stop |
Select-Object @{N="Zone"; E={ $zone.ZoneName }}, *
}
catch {
Write-Warning "Failed to query zone $($zone.ZoneName): $_"
}
}
Write-Host "Total records retrieved: $($allRecords.Count)"
Handling Different Record Types
DNS resource records have type-specific data structures. An A record’s IP address lives in RecordData.IPv4Address, while a CNAME’s target is in RecordData.HostNameAlias and an MX’s exchange server is in RecordData.MailExchange. To create a flat exportable table, you need a type switch that extracts the relevant data field for each record type. A single RecordValue column that normalizes all types to a string makes the CSV universally readable.
$normalizedRecords = $allRecords | ForEach-Object {
$value = switch ($_.RecordType) {
"A" { $_.RecordData.IPv4Address.ToString() }
"AAAA" { $_.RecordData.IPv6Address.ToString() }
"CNAME" { $_.RecordData.HostNameAlias }
"MX" { "$($_.RecordData.Preference) $($_.RecordData.MailExchange)" }
"TXT" { ($_.RecordData.DescriptiveText -join " ") }
"PTR" { $_.RecordData.PtrDomainName }
"NS" { $_.RecordData.NameServer }
"SOA" { "$($_.RecordData.PrimaryServer) / $($_.RecordData.ResponsiblePerson)" }
default { $_.RecordData.ToString() }
}
[PSCustomObject]@{
Zone = $_.Zone
Name = $_.HostName
RecordType = $_.RecordType
TTL = $_.TimeToLive.TotalSeconds
RecordValue = $value
Timestamp = $_.Timestamp
}
}
Write-Host "Record types present: $(($normalizedRecords | Select-Object -Unique RecordType).RecordType -join ', ')"
Running Across Multiple DNS Servers
Enterprise environments typically have multiple DNS servers — primary and secondary, sometimes across sites. Querying all of them reveals discrepancies between zone copies, where a secondary might be out of sync with the primary. Loop over your DNS server list, tag each record with its source server, and combine the results into a unified dataset for comparison.
$dnsServers = @("DC01","DC02","DNS-SECONDARY")
$combinedRecords = [System.Collections.Generic.List[object]]::new()
foreach ($server in $dnsServers) {
Write-Host "Querying DNS server: $server"
$serverZones = Get-DnsServerZone -ComputerName $server -ErrorAction SilentlyContinue |
Where-Object { $_.ZoneType -eq "Primary" -and -not $_.IsAutoCreated }
foreach ($zone in $serverZones) {
$recs = Get-DnsServerResourceRecord -ZoneName $zone.ZoneName `
-ComputerName $server -ErrorAction SilentlyContinue
foreach ($r in $recs) {
$combinedRecords.Add([PSCustomObject]@{
Server = $server
Zone = $zone.ZoneName
HostName = $r.HostName
RecordType = $r.RecordType
})
}
}
}
Write-Host "Total records across all servers: $($combinedRecords.Count)"
Comparing Current Export to Previous Baseline for Changes
A single export is a snapshot. The real value comes from comparing today’s export to the previous one. Compare-Object surfaces records that were added (=>) or removed (<=) between runs. Schedule the export nightly and keep 30 days of baselines. Changes outside of approved maintenance windows are immediate candidates for review.
$today = "C:\DNSAudit\DNS_Export_$(Get-Date -Format yyyyMMdd).csv"
$yesterday = "C:\DNSAudit\DNS_Export_$((Get-Date).AddDays(-1) | Get-Date -Format yyyyMMdd).csv"
$normalizedRecords | Export-Csv $today -NoTypeInformation
Write-Host "Today's export saved: $today"
if (Test-Path $yesterday) {
$baseline = Import-Csv $yesterday
$current = Import-Csv $today
$diff = Compare-Object $baseline $current `
-Property Zone, Name, RecordType, RecordValue
if ($diff) {
Write-Host "DNS changes detected since yesterday:"
$diff | Format-Table -AutoSize
}
else {
Write-Host "No DNS record changes since yesterday."
}
}
else {
Write-Host "No baseline found for comparison — today's export will serve as tomorrow's baseline."
}
Common Errors
- DnsServer module not installed on non-DC machines. The DnsServer module is part of RSAT DNS Tools, not installed by default on management workstations. Install it with
Add-WindowsCapability -Name Rsat.Dns.Tools~~~~0.0.1.0 -Onlineon Windows 10/11, or viaInstall-WindowsFeature RSAT-DNS-Serveron Server editions. Without it,Import-Module DnsServerfails silently or throws an error depending on the PowerShell version. - Get-DnsServerResourceRecord can be slow on large zones. Querying a zone with 50,000+ records without filtering takes significant time and memory. Use
-ZoneNameto scope queries to specific zones rather than querying the server root. If you need only specific record types, the-RRTypeparameter filters at the server side before data is returned, dramatically reducing transfer size.
Related Cmdlets / See Also
Wrapping Up
Regular DNS zone exports provide the change visibility that manual DNS management never can. Export all zones nightly, normalize all record types to a flat table, and run a Compare-Object diff against the previous baseline to surface unauthorised or unexpected changes. The CSV archive doubles as a disaster-recovery reference if you ever need to rebuild a zone from scratch.


