How to Automatically Create Missing PTR Records for A Records in Windows DNS

In large Windows DNS infrastructures, it is common to find A records without corresponding PTR records.

This typically happens when:

  • Network devices (routers, switches, printers, firewalls) cannot perform dynamic DNS updates
  • A records are created manually
  • Reverse zones exist but are not properly maintained
  • Legacy systems were migrated without reverse cleanup

Over time, this leads to inconsistent forward/reverse DNS resolution.

This article describes a production PowerShell script that:

  • Scans all A records in a forward zone
  • Detects missing or mismatched PTR records
  • Automatically creates or updates PTR records
  • Enables aging and timestamp
  • Logs all actions to CSV

The Script

# Full script: Checks and creates PTR records for all A records in the forward zone.
# Automatic mode: No confirmations, all actions logged to CSV.
# Requirements: Import-Module DnsServer. Run as admin on Windows Server.

$ForwardZone = "domain.com"  # Forward zone name
$DNSServer = "dns-server.domain.com"  # DNS server FQDN
$CSVPath = "D:\DNS\DNS_PTR_Changes.csv"  # Path to CSV log file

# Function: Calculates dynamic TTL (remaining time until the end of the hour)
function Get-DynamicTTL {
    $Now = Get-Date
    $EndOfHour = $Now.AddHours(1).AddMinutes(-$Now.Minute).AddSeconds(-$Now.Second).AddMilliseconds(-$Now.Millisecond)
    $DynamicTTL = $EndOfHour - $Now
    return $DynamicTTL  # Returns TimeSpan, e.g., 00:37:00
}

# Function: Finds reverse zone for an IP (/24 > /16)
function Get-ReverseZoneForIP {
    param([string]$IP, [string]$Server)
    
    $octets = $IP -split '\.' | ForEach-Object { [int]$_ }
    if ($octets.Count -ne 4) { return $null }
    
    $candidates = @(
        @{Zone="$($octets[2]).$($octets[1]).$($octets[0]).in-addr.arpa"; Host="$($octets[3])"; Level=24},  # /24 zone
        @{Zone="$($octets[1]).$($octets[0]).in-addr.arpa"; Host="$($octets[3]).$($octets[2])"; Level=16}    # /16 zone
    )
    
    # Sort by specificity (/24 > /16)
    $sortedCandidates = $candidates | Sort-Object Level -Descending
    
    foreach ($cand in $sortedCandidates) {
        $zoneObj = Get-DnsServerZone -Name $cand.Zone -ComputerName $Server -ErrorAction SilentlyContinue
        if ($zoneObj) {
            return @{Zone = $cand.Zone; Host = $cand.Host; FullRevName = "$($cand.Host).$($cand.Zone)"}
        }
    }
    return $null  # No matching reverse zone found
}

# Create CSV headers if file doesn't exist
if (-not (Test-Path $CSVPath)) {
    $csvHeader = @("FQDN", "IP", "ReverseZone", "HostInZone", "Action", "Status", "ErrorMsg", "ScriptRunTime")
    $csvHeader | Out-File -FilePath $CSVPath -Encoding UTF8
}

# Get all A records from the forward zone (exclude SOA, NS, wildcard)
$A_Records = Get-DnsServerResourceRecord -ZoneName $ForwardZone -RRType A -ComputerName $DNSServer | 
             Where-Object { $_.HostName -ne '@' -and $_.HostName -ne '*' -and $_.RecordType -eq 'A' }

$processedCount = 0
$createdCount = 0
$skippedCount = 0
$errorCount = 0

Write-Host "Starting processing of $($A_Records.Count) A records in zone $ForwardZone..." -ForegroundColor Cyan
$startTime = Get-Date

foreach ($Record in $A_Records) {
    $FQDN = if ($Record.HostName -eq '@') { $ForwardZone } else { "$($Record.HostName).$ForwardZone" }
    $IP = $Record.RecordData.IPv4Address.IPAddressToString
    $processedCount++
    
    # Log initial processing
    $logEntry = [PSCustomObject]@{
        FQDN = $FQDN
        IP = $IP
        ReverseZone = ""
        HostInZone = ""
        Action = "Processing"
        Status = "In Progress"
        ErrorMsg = ""
        ScriptRunTime = $startTime.ToString("yyyy-MM-dd HH:mm:ss")
    }
    $logEntry | Export-Csv -Path $CSVPath -NoTypeInformation -Append -Encoding UTF8
    
    # Find reverse zone for IP
    $revInfo = Get-ReverseZoneForIP -IP $IP -Server $DNSServer
    if (-not $revInfo) {
        $logEntry.ReverseZone = "NO ZONE FOUND"
        $logEntry.Action = "Skipped"
        $logEntry.Status = "Error"
        $logEntry.ErrorMsg = "No matching reverse zone (/24 or /16)"
        $logEntry | Export-Csv -Path $CSVPath -NoTypeInformation -Append -Encoding UTF8
        $errorCount++
        Write-Host "[$processedCount/$($A_Records.Count)] Skipped $FQDN ($IP): No reverse zone." -ForegroundColor Yellow
        continue
    }
    
    # Check for existing PTR in the reverse zone
    $existingPTR = Get-DnsServerResourceRecord -ZoneName $revInfo.Zone -Name $revInfo.Host -RRType Ptr -ComputerName $DNSServer -ErrorAction SilentlyContinue
    $PTR_Name = if ($existingPTR) { $existingPTR.RecordData.PtrDomainName.TrimEnd('.') } else { $null }
    
    $action = if (-not $PTR_Name) { "Created" } elseif ($PTR_Name -ne $FQDN) { "Updated (Mismatch)" } else { "Skipped (OK)" }
    
    if ($action -eq "Skipped (OK)") {
        $logEntry.ReverseZone = $revInfo.Zone
        $logEntry.HostInZone = $revInfo.Host
        $logEntry.Action = "Skipped"
        $logEntry.Status = "Success"
        $logEntry.ErrorMsg = "PTR already exists and matches"
        $logEntry | Export-Csv -Path $CSVPath -NoTypeInformation -Append -Encoding UTF8
        $skippedCount++
        Write-Host "[$processedCount/$($A_Records.Count)] OK $FQDN ($IP): PTR already correct." -ForegroundColor Green
        continue
    }
    
    # Create/Update PTR with dynamic TTL and Timestamp using dnscmd
    $DynamicTTL = Get-DynamicTTL
    $TTLSeconds = [math]::Ceiling($DynamicTTL.TotalSeconds)  # Round to whole seconds
    # For fixed TTL (3600 seconds), uncomment: $TTLSeconds = 3600
    
    $logEntry.ReverseZone = $revInfo.Zone
    $logEntry.HostInZone = $revInfo.Host
    
    try {
        # Remove old PTR if mismatch
        if ($PTR_Name) {
            Remove-DnsServerResourceRecord -ZoneName $revInfo.Zone -Name $revInfo.Host -RRType Ptr -ComputerName $DNSServer -Force
        }
        
        # Create new PTR using dnscmd
        $FQDNWithDot = "$FQDN."
        $cmd = "dnscmd $DNSServer /RecordAdd $($revInfo.Zone) $($revInfo.Host) /Aging $TTLSeconds PTR $FQDNWithDot"
        $dnscmdOutput = Invoke-Expression $cmd 2>&1
        if ($LASTEXITCODE -eq 0) {
            $logEntry.Action = $action
            $logEntry.Status = "Success"
            $logEntry.ErrorMsg = "PTR created/updated with TTL $DynamicTTL"
            $createdCount++
            Write-Host "[$processedCount/$($A_Records.Count)] ✅ $action $FQDN ($IP) in $($revInfo.Zone) ($($revInfo.Host))" -ForegroundColor Green
        } else {
            throw "dnscmd failed: $dnscmdOutput"
        }
    }
    catch {
        $logEntry.Action = "Error"
        $logEntry.Status = "Error"
        $logEntry.ErrorMsg = $_.Exception.Message
        $errorCount++
        Write-Host "[$processedCount/$($A_Records.Count)] ❌ Error for $FQDN ($IP): $($_.Exception.Message)" -ForegroundColor Red
    }
    
    # Update CSV log
    $logEntry | Export-Csv -Path $CSVPath -NoTypeInformation -Append -Encoding UTF8
}

$endTime = Get-Date
$duration = $endTime - $startTime

Write-Host "`nProcessing completed!" -ForegroundColor Cyan
Write-Host "Total records: $processedCount" -ForegroundColor White
Write-Host "Created/Updated: $createdCount" -ForegroundColor Green
Write-Host "Skipped (OK): $skippedCount" -ForegroundColor Gray
Write-Host "Errors: $errorCount" -ForegroundColor Red
Write-Host "Execution time: $duration" -ForegroundColor White
Write-Host "Log file: $CSVPath" -ForegroundColor Yellow
Write-Host "Open CSV in Excel to review or fix issues." -ForegroundColor Yellow

# Optional: Clear cache for all reverse zones
Write-Host "`nClear cache for all reverse zones? (Y/N) — manual confirmation for safety" -ForegroundColor Cyan
$clearCache = Read-Host
if ($clearCache -eq 'Y' -or $clearCache -eq 'y') {
    Get-DnsServerZone -ComputerName $DNSServer | Where-Object { $_.IsReverseLookupZone } | ForEach-Object { 
        Clear-DnsServerCache -ZoneName $_.ZoneName -ComputerName $DNSServer -ErrorAction SilentlyContinue
    }
    Write-Host "Cache cleared for all reverse zones." -ForegroundColor Green
}

Why Missing PTR Records Are a Problem

Missing reverse records can cause:

  • SMTP reverse lookup failures
  • Monitoring / SIEM hostname resolution issues
  • Troubleshooting delays
  • Kerberos validation problems
  • Inconsistent DNS infrastructure

Example:

nslookup 10.10.10.5

If no PTR exists, reverse lookup fails, which complicates diagnostics and logging.

When This Script Is Useful

Use this script if:

  • You manage static DNS zones
  • Network devices cannot register PTR automatically
  • You inherited legacy DNS infrastructure
  • You want to audit forward/reverse consistency
  • You need bulk remediation instead of manual fixes

This is especially useful in environments with multiple DNS servers and manually maintained zones

Requirements

  • Windows Server with DNS role installed
  • PowerShell DnsServer module available
  • Administrative privileges
  • Existing reverse zones (/24 or /16 supported)

Script Configuration

Before running the script, configure:

$ForwardZone = "domain.com"
$DNSServer   = "dns-server.domain.com"
$CSVPath     = "D:\DNS\DNS_PTR_Changes.csv"

Parameters explained:

  • $ForwardZone — Forward lookup zone to scan
  • $DNSServer — Target DNS server (FQDN)
  • $CSVPath — Path where the execution log will be stored

How the Script Works

Retrieves All A Records

The script pulls all A records from the forward zone and excludes:

  • SOA
  • NS
  • Wildcard entries

Automatically Detects the Correct Reverse Zone

The script dynamically checks:

  • /24 reverse zone
  • /16 reverse zone

It prefers the most specific match (/24 first).

This avoids hardcoding reverse zone names and makes the script portable.

Validates Existing PTR Records

For each A record:

  • If PTR exists and matches → Skipped
  • If PTR exists but mismatched → Updated
  • If no PTR exists → Created

Dynamic TTL Calculation

Instead of using a static TTL, the script calculates a dynamic TTL based on the remaining time until the end of the current hour.

This keeps reverse records aligned and avoids arbitrary TTL values.

If you prefer fixed TTL, you can hardcode it (e.g., 3600 seconds).

PTR Creation with Aging Enabled

The script uses:

dnscmd /RecordAdd ... /Aging

This ensures:

  • Timestamp is enabled
  • Record is not static
  • Scavenging works correctly

Full CSV Logging

Every action is logged to CSV, including:

  • FQDN
  • IP address
  • Reverse zone
  • Host in zone
  • Action performed
  • Status
  • Error message
  • Script runtime

This allows post-execution auditing in Excel.

Execution

Run the script as Administrator:

.\Fix-MissingPTR.ps1

At completion, you will see:

  • Total processed records
  • Created/Updated count
  • Skipped records
  • Errors
  • Execution time
  • CSV log location

Safety Considerations

Before running in production:

  • Ensure reverse zones exist
  • Test in lab environment
  • Backup DNS server if performing bulk operations
  • Review CSV output carefully
  • Consider implementing a -WhatIf mode for dry-run

Leave a Comment