
Automate DNS Record Audits on Windows DNS with PowerShell
Before you run this
This guide builds a read-only audit of a Microsoft DNS server: it enumerates the zones, exports every resource record to CSV, and produces a short report of records that look stale (dynamic records whose aging timestamp is older than a threshold you set). The audit script itself creates nothing and deletes nothing — its worst case is a CSV file on disk.
- Privileges: the
DnsServermodule cmdlets query the DNS service, so run this from an elevated PowerShell session (Run as administrator) as an account with rights to read the DNS server — typically a member of the local Administrators group on the DNS server, or DnsAdmins. You can run it on the DNS server, or remotely from a management box that has the RSAT DNS tools installed by passing-ComputerName. - Module: you need the
DnsServerPowerShell module. It is present when the DNS Server role is installed, or via RSAT: DNS Server Tools on a management workstation. - Test first: read the script before you run it. Run it against a single test zone first (set
$Zonesto one lab zone) and confirm the CSV looks sane before you sweep every zone on a production server. - The cleanup step is destructive and immediate. The last section shows how to remove stale records with
Remove-DnsServerResourceRecord. There is no recycle bin for DNS records — a deleted record is gone until you recreate it by hand or restore the zone. That step is gated behind-WhatIfand a confirmation. Do not remove anything until you have exported the zone (below) and reviewed the audit CSV with whoever owns the affected hosts.
Assumed environment: Windows Server 2019 or 2022 with the DNS Server role, an Active Directory–integrated primary zone, and Windows PowerShell 5.1 (the version that ships in-box). The cmdlets are the same in PowerShell 7 with the DnsServer module imported.
Back up the zone before you touch anything
The audit is read-only, but if you intend to act on the results, export each zone to a file first. Export-DnsServerZone writes a zone export under the DNS server's file directory (by default %SystemRoot%\System32\dns):
# Run on the DNS server, elevated. Writes to %SystemRoot%\System32\dns\
Export-DnsServerZone -Name "example.com" -FileName "example.com.audit.bak"
For an AD-integrated zone this export is your quick reference copy; a full recovery of AD-integrated zones comes from your Active Directory / system-state backup. Confirm you have that backup before any cleanup.
Step 1 — Confirm the module and list your zones
# Load the DNS cmdlets and check what's available
Import-Module DnsServer
Get-Command -Module DnsServer | Select-Object Name | Sort-Object Name
List the zones so you know your scope. This filters out the auto-created reverse zones you rarely care about:
$DnsServer = "dns01.example.com" # replace with your DNS server, or "localhost"
Get-DnsServerZone -ComputerName $DnsServer |
Where-Object { -not $_.IsAutoCreated } |
Select-Object ZoneName, ZoneType, IsReverseLookupZone, IsDsIntegrated |
Format-Table -AutoSize
Step 2 — Export every record to CSV
Get-DnsServerResourceRecord returns record objects whose RecordData is a type-specific sub-object — an A record exposes RecordData.IPv4Address, a CNAME exposes RecordData.HostNameAlias, and so on. To get a flat, readable CSV I normalise the common record types into a single Data column. Types I don't explicitly handle still export with their name, timestamp and TTL; their Data is left blank rather than guessed.
$DnsServer = "dns01.example.com" # replace with your server
$OutputDir = "C:\path\to\dns-audit" # replace with a real folder
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
# Turn a record's type-specific RecordData into one readable string.
function Get-RecordValue {
param($Record)
$d = $Record.RecordData
switch ($Record.RecordType) {
'A' { $d.IPv4Address.IPAddressToString }
'AAAA' { $d.IPv6Address.IPAddressToString }
'CNAME' { $d.HostNameAlias }
'PTR' { $d.PtrDomainName }
'NS' { $d.NameServer }
'MX' { "$($d.Preference) $($d.MailExchange)" }
'SRV' { "$($d.Priority) $($d.Weight) $($d.Port) $($d.DomainName)" }
'TXT' { $d.DescriptiveText }
default { '' } # unhandled type: leave blank rather than guess
}
}
$zones = Get-DnsServerZone -ComputerName $DnsServer |
Where-Object { -not $_.IsAutoCreated }
$report = foreach ($zone in $zones) {
Get-DnsServerResourceRecord -ComputerName $DnsServer -ZoneName $zone.ZoneName |
ForEach-Object {
[PSCustomObject]@{
Zone = $zone.ZoneName
HostName = $_.HostName
Type = $_.RecordType
Data = Get-RecordValue $_
TTL = $_.TimeToLive
# Timestamp is set only on dynamically-registered records;
# static records have no timestamp.
Timestamp = $_.Timestamp
}
}
}
$csv = Join-Path $OutputDir ("dns-inventory-{0:yyyyMMdd}.csv" -f (Get-Date))
$report | Export-Csv -Path $csv -NoTypeInformation -Encoding UTF8
Write-Host "Wrote $($report.Count) records to $csv"
Open the CSV. This is your full inventory and the thing you keep on file.
Step 3 — Flag stale dynamic records
Dynamically registered records carry a Timestamp that DNS refreshes when the client re-registers. Static records (ones you created by hand) have no timestamp and must never be treated as stale by age. This report lists dynamic records whose timestamp is older than a threshold — the same records scavenging would eventually target:
$StaleDays = 30 # tune to your refresh/no-refresh interval
$cutoff = (Get-Date).AddDays(-$StaleDays)
$stale = $report | Where-Object {
$_.Timestamp -ne $null -and $_.Timestamp -lt $cutoff
}
$staleCsv = Join-Path $OutputDir ("dns-stale-{0:yyyyMMdd}.csv" -f (Get-Date))
$stale | Sort-Object Timestamp |
Export-Csv -Path $staleCsv -NoTypeInformation -Encoding UTF8
Write-Host "$($stale.Count) records older than $StaleDays days -> $staleCsv"
Before you trust the age, check that aging/scavenging is actually configured — otherwise timestamps may not mean what you think:
Get-DnsServerScavenging -ComputerName $DnsServer
Get-DnsServerZoneAging -ComputerName $DnsServer -Name "example.com"
See Microsoft Learn for Get-DnsServerScavenging and Get-DnsServerZoneAging if you need to interpret the refresh/no-refresh intervals.
Step 4 (optional, destructive) — remove reviewed stale records
Only do this after the stale CSV has been reviewed and signed off. Remove-DnsServerResourceRecord supports -WhatIf; run it that way first and read every line of output. There is no undo other than recreating the record or restoring the zone.
# DRY RUN — nothing is deleted. Review the output carefully.
foreach ($r in $stale) {
Remove-DnsServerResourceRecord -ComputerName $DnsServer `
-ZoneName $r.Zone -Name $r.HostName -RRType $r.Type `
-WhatIf
}
When you are certain, remove -WhatIf. I keep -Confirm on so each deletion is an explicit yes:
foreach ($r in $stale) {
Remove-DnsServerResourceRecord -ComputerName $DnsServer `
-ZoneName $r.Zone -Name $r.HostName -RRType $r.Type `
-Confirm
}
If a name has multiple records (for example, several A records under one host), Remove-DnsServerResourceRecord will prompt for the specific record; match it against your CSV. Check Microsoft Learn for the -RecordData parameter set if you need to target one exact record among several.
Verify
- Inventory: open the CSV and spot-check a few hosts you know against the live server:
Get-DnsServerResourceRecord -ComputerName $DnsServer -ZoneName "example.com" -Name "www". - After a removal: confirm the record is gone with
Resolve-DnsName www.example.com -Server $DnsServer(should return no answer for that name/type) andGet-DnsServerResourceRecordfor the same host. - Undo a wrongful deletion: recreate the record with the matching
Add-DnsServerResourceRecord...cmdlet (Add-DnsServerResourceRecordA,-CName, etc.), using theData,TTLand host from your inventory CSV — that CSV is exactly why you kept it. For a larger mistake, restore the zone from the export in Step 0 or from your AD/system-state backup.
Schedule Steps 1–3 as a read-only weekly task (Task Scheduler running powershell.exe -File), and keep Step 4 as a manual, reviewed action. The audit is the automation; the deletion stays human.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
