Set-NTPConfig.psm1
<#
.SYNOPSIS Configures Windows NTP client with secure, reliable time synchronization. .DESCRIPTION Sets NTP servers, configures polling intervals, and ensures Windows Time service is properly configured. Includes error handling, validation, and geographic detection. .PARAMETER NtpServers Comma-separated list of NTP servers. If not specified, automatically detects region. .PARAMETER Region Geographic region for NTP pool selection. Options: NorthAmerica, Europe, Asia, Oceania, SouthAmerica, Africa, Auto. Default is Auto (detects based on timezone). .PARAMETER SpecialPollInterval Poll interval in seconds. Default is 900 (15 minutes) for workstations, 300 (5 minutes) for servers. Valid range: 64-86400. Lower values increase accuracy but also network traffic. .PARAMETER ServerType System type. Options: Workstation, Server. Adjusts default poll interval accordingly. .PARAMETER Force Skip confirmation prompts. .EXAMPLE Set-NTPConfig Automatically detects region and configures with appropriate defaults. .EXAMPLE Set-NTPConfig -Region Europe -SpecialPollInterval 300 Uses European NTP pool servers with 5-minute polling. .EXAMPLE Set-NTPConfig -NtpServers "time.cloudflare.com,0x9 time.google.com,0x9" -Force Uses custom NTP servers without confirmation. .EXAMPLE Set-NTPConfig -ServerType Server Configures for server with 5-minute default polling interval. .NOTES Requires Administrator privileges. Author: Collin George Version: 2.0.5 Last Updated: October 2, 2025 License: MIT License MIT License: Copyright (c) 2025 Collin George Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Changelog: - v2.0.5 (2025-10-02): Fixed syntax error in Write-Log function. - v2.0.4 (2025-10-02): Moved main logic into Set-NTPConfig function to fix $PSCmdlet error on import. - v2.0.3 (2025-10-02): Moved SpecialPollInterval validation to script body. - v2.0.2 (2025-10-02): Attempted fix using Nullable[int] for SpecialPollInterval. - v2.0.1 (2025-10-02): Attempted fix for SpecialPollInterval validation error. - v2.0 (2025-10-01): Added MIT License, enhanced error handling, region-based NTP pools. - v1.0: Initial release. #> function Write-Log { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$Message, [Parameter(Mandatory=$false)] [ValidateSet('Info', 'Warning', 'Error', 'Success')] [string]$Level = 'Info' ) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $color = switch ($Level) { 'Info' { 'Cyan' } 'Warning' { 'Yellow' } 'Error' { 'Red' } 'Success' { 'Green' } } Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color } function Get-RegionFromTimezone { [CmdletBinding()] param() try { $timezone = Get-TimeZone $timezoneId = $timezone.Id switch -Regex ($timezoneId) { 'Pacific|Mountain|Central|Eastern|Alaska|Hawaii|US|Canada|Mexico' { return 'NorthAmerica' } 'Europe|GMT|UTC|London|Paris|Berlin|Rome|Madrid' { return 'Europe' } 'Asia|Tokyo|Seoul|Shanghai|Hong Kong|Singapore|India' { return 'Asia' } 'Australia|New Zealand|Pacific/Auckland' { return 'Oceania' } 'South America|Argentina|Brazil|Chile' { return 'SouthAmerica' } 'Africa|Cairo|Johannesburg' { return 'Africa' } default { Write-Log "Could not detect region from timezone: $timezoneId. Defaulting to NorthAmerica." -Level Warning return 'NorthAmerica' } } } catch { Write-Log "Error detecting timezone: $_. Defaulting to NorthAmerica." -Level Warning return 'NorthAmerica' } } function Get-NtpServersForRegion { [CmdletBinding()] param([string]$Region) $ntpPools = @{ 'NorthAmerica' = "0.north-america.pool.ntp.org,0x9 1.north-america.pool.ntp.org,0x9 2.north-america.pool.ntp.org,0x9 3.north-america.pool.ntp.org,0x9" 'Europe' = "0.europe.pool.ntp.org,0x9 1.europe.pool.ntp.org,0x9 2.europe.pool.ntp.org,0x9 3.europe.pool.ntp.org,0x9" 'Asia' = "0.asia.pool.ntp.org,0x9 1.asia.pool.ntp.org,0x9 2.asia.pool.ntp.org,0x9 3.asia.pool.ntp.org,0x9" 'Oceania' = "0.oceania.pool.ntp.org,0x9 1.oceania.pool.ntp.org,0x9 2.oceania.pool.ntp.org,0x9 3.oceania.pool.ntp.org,0x9" 'SouthAmerica' = "0.south-america.pool.ntp.org,0x9 1.south-america.pool.ntp.org,0x9 2.south-america.pool.ntp.org,0x9 3.south-america.pool.ntp.org,0x9" 'Africa' = "0.africa.pool.ntp.org,0x9 1.africa.pool.ntp.org,0x9 2.africa.pool.ntp.org,0x9 3.africa.pool.ntp.org,0x9" } return $ntpPools[$Region] } function Test-RegistryPath { [CmdletBinding()] param([string]$Path) try { return Test-Path -Path $Path -ErrorAction Stop } catch { Write-Log "Failed to access registry path: $Path" -Level Error return $false } } function Set-RegistryValue { [CmdletBinding()] param( [string]$Path, [string]$Name, [object]$Value, [string]$Type = 'String' ) try { if (-not (Test-RegistryPath -Path $Path)) { Write-Log "Registry path does not exist: $Path" -Level Error throw "Registry path not found" } Set-ItemProperty -Path $Path -Name $Name -Value $Value -Type $Type -ErrorAction Stop Write-Log "Set $Name = $Value at $Path" -Level Success return $true } catch { Write-Log "Failed to set registry value $Name at $Path : $_" -Level Error throw } } function Wait-ServiceState { [CmdletBinding()] param( [string]$ServiceName, [string]$DesiredState, [int]$TimeoutSeconds = 30 ) $elapsed = 0 $intervalMs = 500 while ($elapsed -lt ($TimeoutSeconds * 1000)) { $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($service.Status -eq $DesiredState) { return $true } Start-Sleep -Milliseconds $intervalMs $elapsed += $intervalMs } return $false } function Test-NtpServer { [CmdletBinding()] param([string]$Server) try { Write-Log "Testing connectivity to $Server..." -Level Info $result = w32tm /stripchart /computer:$Server /samples:1 /dataonly 2>&1 if ($LASTEXITCODE -eq 0) { Write-Log "Successfully contacted $Server" -Level Success return $true } else { Write-Log "Could not reach $Server" -Level Warning return $false } } catch { Write-Log "Error testing $Server : $_" -Level Warning return $false } } function Set-NTPConfig { [CmdletBinding(SupportsShouldProcess=$true)] param( [Parameter(Mandatory=$false)] [string]$NtpServers, [Parameter(Mandatory=$false)] [ValidateSet('NorthAmerica', 'Europe', 'Asia', 'Oceania', 'SouthAmerica', 'Africa', 'Auto')] [string]$Region = 'Auto', [Parameter(Mandatory=$false)] [Nullable[int]]$SpecialPollInterval = $null, [Parameter(Mandatory=$false)] [ValidateSet('Workstation', 'Server')] [string]$ServerType = 'Workstation', [Parameter(Mandatory=$false)] [switch]$Force ) #Requires -RunAsAdministrator # Set strict mode for better error detection Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' try { Write-Log "=== Windows NTP Configuration Script v2.0.5 ===" -Level Info Write-Log "Starting NTP configuration..." -Level Info # Validate SpecialPollInterval if ($PSBoundParameters.ContainsKey('SpecialPollInterval') -and $null -ne $SpecialPollInterval) { if ($SpecialPollInterval -lt 64 -or $SpecialPollInterval -gt 86400) { Write-Log "SpecialPollInterval must be between 64 and 86400 seconds. Got: $SpecialPollInterval" -Level Error throw "Invalid SpecialPollInterval value" } } # Set default poll interval based on server type if not specified if (-not $PSBoundParameters.ContainsKey('SpecialPollInterval') -or $null -eq $SpecialPollInterval) { $SpecialPollInterval = if ($ServerType -eq 'Server') { 300 } else { 900 } Write-Log "Using default poll interval for ${ServerType}: $SpecialPollInterval seconds" -Level Info } # Determine region if auto-detect if (-not $NtpServers) { if ($Region -eq 'Auto') { $Region = Get-RegionFromTimezone Write-Log "Auto-detected region: $Region" -Level Info } $NtpServers = Get-NtpServersForRegion -Region $Region Write-Log "Using $Region NTP pool servers" -Level Info } else { Write-Log "Using custom NTP servers" -Level Info } Write-Log "NTP Servers: $NtpServers" -Level Info Write-Log "Poll Interval: $SpecialPollInterval seconds ($([Math]::Round($SpecialPollInterval/60, 1)) minutes)" -Level Info # Test connectivity to first NTP server $firstServer = ($NtpServers -split ' ')[0] -replace ',0x9', '' Test-NtpServer -Server $firstServer # Backup current configuration Write-Log "`nBacking up current configuration..." -Level Info $w32timeParams = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters" $currentNtpServer = (Get-ItemProperty -Path $w32timeParams -Name "NtpServer" -ErrorAction SilentlyContinue).NtpServer $currentType = (Get-ItemProperty -Path $w32timeParams -Name "Type" -ErrorAction SilentlyContinue).Type if ($currentNtpServer) { Write-Log "Current NTP Server: $currentNtpServer" -Level Info Write-Log "Current Type: $currentType" -Level Info } else { Write-Log "No existing NTP configuration found" -Level Info } # Confirm changes if not forced if (-not $Force -and $PSCmdlet.ShouldProcess("Windows Time Service", "Configure NTP settings")) { Write-Host "`nThis will configure Windows Time with the following settings:" -ForegroundColor Yellow Write-Host " Region: $Region" -ForegroundColor White Write-Host " NTP Servers: $NtpServers" -ForegroundColor White Write-Host " Poll Interval: $SpecialPollInterval seconds" -ForegroundColor White Write-Host "" $continue = Read-Host "Continue with configuration? (Y/N)" if ($continue -ne 'Y') { Write-Log "Configuration cancelled by user." -Level Warning return } } # 1. Configure W32Time Parameters Write-Log "`nConfiguring W32Time parameters..." -Level Info Set-RegistryValue -Path $w32timeParams -Name "NtpServer" -Value $NtpServers Set-RegistryValue -Path $w32timeParams -Name "Type" -Value "NTP" # 2. Configure NtpClient Provider Write-Log "Configuring NtpClient provider..." -Level Info $ntpClientPath = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient" Set-RegistryValue -Path $ntpClientPath -Name "SpecialPollInterval" -Value $SpecialPollInterval -Type DWord Set-RegistryValue -Path $ntpClientPath -Name "Enabled" -Value 1 -Type DWord # 3. Configure additional reliability settings Write-Log "Configuring additional time service settings..." -Level Info $configPath = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" Set-RegistryValue -Path $configPath -Name "MaxPosPhaseCorrection" -Value 3600 -Type DWord Set-RegistryValue -Path $configPath -Name "MaxNegPhaseCorrection" -Value 3600 -Type DWord Set-RegistryValue -Path $configPath -Name "UpdateInterval" -Value 100 -Type DWord # 4. Configure Windows Time Service Write-Log "Configuring Windows Time service..." -Level Info $service = Get-Service -Name w32time -ErrorAction Stop if ($service.StartType -ne 'Automatic') { Set-Service -Name w32time -StartupType Automatic -ErrorAction Stop Write-Log "Set w32time service to Automatic startup" -Level Success } # 5. Restart Service Write-Log "Restarting Windows Time service..." -Level Info if ($service.Status -eq 'Running') { Stop-Service -Name w32time -Force -ErrorAction Stop if (-not (Wait-ServiceState -ServiceName w32time -DesiredState Stopped -TimeoutSeconds 30)) { throw "Service did not stop within timeout period" } Write-Log "Service stopped successfully" -Level Success } Start-Service -Name w32time -ErrorAction Stop if (-not (Wait-ServiceState -ServiceName w32time -DesiredState Running -TimeoutSeconds 30)) { throw "Service did not start within timeout period" } Write-Log "Service started successfully" -Level Success # 6. Apply Configuration and Sync Write-Log "Applying configuration changes..." -Level Info $updateResult = w32tm /config /update 2>&1 if ($LASTEXITCODE -ne 0) { Write-Log "w32tm /config /update returned non-zero exit code: $LASTEXITCODE" -Level Warning Write-Log "Output: $updateResult" -Level Warning } else { Write-Log "Configuration updated successfully" -Level Success } # Wait for service to process configuration Start-Sleep -Seconds 5 Write-Log "Forcing immediate time synchronization..." -Level Info $resyncResult = w32tm /resync /rediscover 2>&1 if ($LASTEXITCODE -eq 0) { Write-Log "Time synchronization initiated successfully" -Level Success } else { Write-Log "Resync returned code $LASTEXITCODE - this may be normal if sync is in progress" -Level Warning } # 7. Verify Configuration Write-Log "`n=== VERIFICATION ===" -Level Info $verifyNtpServer = (Get-ItemProperty -Path $w32timeParams -Name "NtpServer").NtpServer $verifyType = (Get-ItemProperty -Path $w32timeParams -Name "Type").Type $verifyPollInterval = (Get-ItemProperty -Path $ntpClientPath -Name "SpecialPollInterval").SpecialPollInterval $verifyService = Get-Service -Name w32time Write-Host "`nConfigured NTP Servers: " -NoNewline Write-Host $verifyNtpServer -ForegroundColor Green Write-Host "Poll Interval: " -NoNewline Write-Host "$verifyPollInterval seconds ($([Math]::Round($verifyPollInterval/60, 1)) minutes)" -ForegroundColor Green Write-Host "Service Status: " -NoNewline Write-Host "$($verifyService.Status) ($($verifyService.StartType))" -ForegroundColor Green # 8. Display Current Status Write-Log "`n=== CURRENT TIME SYNCHRONIZATION STATUS ===" -Level Info w32tm /query /status Write-Log "`n=== CONFIGURATION COMPLETE ===" -Level Success Write-Host "`nNTP configuration completed successfully!" -ForegroundColor Green Write-Host "Note: It may take a few minutes for initial time synchronization to complete." -ForegroundColor Yellow Write-Host "`nTo verify sync status later, run: " -NoNewline -ForegroundColor Cyan Write-Host "w32tm /query /status" -ForegroundColor White } catch { Write-Log "`n=== FATAL ERROR ===" -Level Error Write-Log "Error: $_" -Level Error Write-Log "Stack Trace: $($_.ScriptStackTrace)" -Level Error # Attempt to restore service if it's stopped $service = Get-Service -Name w32time -ErrorAction SilentlyContinue if ($service -and $service.Status -ne 'Running') { Write-Log "Attempting to restart Windows Time service..." -Level Warning try { Start-Service -Name w32time -ErrorAction Stop Write-Log "Service restarted successfully" -Level Success } catch { Write-Log "Failed to restart service: $_" -Level Error Write-Log "You may need to manually restart the Windows Time service" -Level Error } } throw } finally { Write-Log "`nScript execution completed." -Level Info } } # Export the main function Export-ModuleMember -Function Set-NTPConfig |