
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.
Related guides
Automate a Windows Server Hardening Baseline Check with PowerShell
This script is a read-only audit . It reports PASS/FAIL for a small set of well-known hardening settings — SMBv1 status, firewall profiles, Defender real-time protection, RDP Network Level Authentication, and the local Guest account — and writes the results to the screen and a CSV. It changes nothing. There is nothing to roll back, which is exactly why an audit script is the safe place to start before you touch any actual configuration.
Automate a Daily Failed-Logon (4625) Report with PowerShell
This guide builds a scheduled PowerShell job that reads Event ID 4625 (failed logon) from the Windows Security log for the last 24 hours and writes them to a dated HTML report. It is read-only — it queries the event log and creates a report file. It does not change auditing policy, delete events, or touch accounts.
Automate a Network Share Permissions Audit with PowerShell
This guide builds a read-only report . The script enumerates the SMB shares on a Windows file server, then lists two things for each one: the share-level permissions (the "who can connect" layer) and the NTFS permissions on the folder behind it (the "who can touch the files" layer). It writes both to CSV so you can review access in a spreadsheet instead of clicking through the Security tab share by share.
Automate Certificate Expiry Checks on Windows with PowerShell
An expired TLS certificate is the kind of outage that is entirely preventable and still catches everyone. This guide builds a small PowerShell script that reads the certificates in a server's own store, flags any that expire soon, and then schedules it to run daily so you hear about it weeks in advance instead of from a monitoring alert at 2 a.m.




