Shore Up
An inspector with a clipboard walking past a long wall of hanging address tags, marking the faded, out-of-date ones with small red flags while the current ones stay untouched.
WindowsSecurity

Automate DNS Record Audits on Windows DNS with PowerShell

Ketan Aagja9 min read
No ratings yet

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 DnsServer module 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 DnsServer PowerShell 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 $Zones to 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 -WhatIf and 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) and Get-DnsServerResourceRecord for the same host.
  • Undo a wrongful deletion: recreate the record with the matching Add-DnsServerResourceRecord... cmdlet (Add-DnsServerResourceRecordA, -CName, etc.), using the Data, TTL and 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.

Written by
Ketan Aagja

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.