Krbtgt.psm1
#Requires -Module ActiveDirectory $script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\Krbtgt.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName Krbtgt.Import.DoDotSource -Fallback $true if ($Krbtgt_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName Krbtgt.Import.IndividualFiles -Fallback $false if ($Krbtgt_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) if ($doDotSource) { . (Resolve-Path $Path) } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText((Resolve-Path $Path)))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'Krbtgt' -Language 'en-US' # Enable Feature Flag: Inherit Enable Exception Set-PSFFeature -Name PSFramework.InheritEnableException -Value $true -ModuleName 'Krbtgt' # Prepare Onetime cache for PDC Emulators Register-PSFTaskEngineTask -Name 'krbtgt.pdccache' -ScriptBlock { Set-PSFTaskEngineCache -Module krbtgt -Name PDCs -Value ((Get-ADForest).Domains | Get-ADDomain).PDCEmulator } -Once # Prepare Onetime cache for DCs of any kind Register-PSFTaskEngineTask -Name 'krbtgt.dccache' -ScriptBlock { $dcHash = @{ } $rodcHash = @{ } foreach ($domain in ((Get-ADForest).Domains | Get-ADDomain)) { try { $dcHash[$domain.DNSRoot] = (Get-ADComputer -Server $domain.PDCEmulator -LDAPFilter '(primaryGroupID=516)').DNSHostName $rodcHash[$domain.DNSRoot] = (Get-ADComputer -Server $domain.PDCEmulator -LDAPFilter '(primaryGroupID=521)').DNSHostName } catch { } } Set-PSFTaskEngineCache -Module krbtgt -Name DCs -Value $dcHash Set-PSFTaskEngineCache -Module krbtgt -Name RODCs -Value $rodcHash } -Once # Enable PSFComputer to understand ADDomainController objects Register-PSFParameterClassMapping -ParameterClass Computer -TypeName 'Microsoft.ActiveDirectory.Management.ADDomainController' -Properties HostName function Get-RODomainController { <# .SYNOPSIS Returns a list of Read Only Domain Controllers. .DESCRIPTION Returns a list of Read Only Domain Controllers. Includes replication and krbtgt account information. .PARAMETER Name A name filter to limit the selection range. .PARAMETER Server The server to retrieve the information from. .EXAMPLE PS C:\> Get-RODomainController Returns information on all RODCs in the current domain. #> [CmdletBinding()] param ( [string] $Name = "*", [string] $Server ) begin { $parameter = @{ LdapFilter = "(&(primaryGroupID=521)(name=$Name))" Properties = 'msDS-KrbTgtLink' } if ($Server) { $parameter["Server"] = $Server } } process { $rodcs = Get-ADComputer @parameter foreach ($rodc in $rodcs) { $domainDN = ($rodc.DistinguishedName -split "," | Where-Object { $_ -like "DC=*" }) -join ',' $siteServerObjects = Get-ADObject -LDAPFilter "(&(objectClass=server)(dnsHostName=$($rodc.DNSHostName)))" -SearchBase "CN=Sites,CN=Configuration,$($domainDN)" $replicationPartner = @() foreach ($siteServerObject in $siteServerObjects) { $fromServer = (Get-ADObject -SearchBase $siteServerObject.DistinguishedName -LDAPFilter '(objectClass=nTDSConnection)' -Properties FromServer).FromServer $replicationPartner += (Get-ADObject $fromServer.Split(",", 2)[1] -Properties dNSHostName).dNSHostName } [PSCustomObject]@{ DistinguishedName = $rodc.DistinguishedName DNSHostName = $rodc.DNSHostName Name = $rodc.Name Enabled = $rodc.Enabled ReplicationPartner = $replicationPartner KerberosAccount = $rodc.'msDS-KrbTgtLink' } } } } function New-Password { <# .SYNOPSIS Generates a random password .DESCRIPTION Generates a random password. Is guaranteed to be complex. .PARAMETER Length The number of characters the password should have. Defaults to 26 .PARAMETER AsSecureString Returns the password as a secure string. .EXAMPLE PS C:\> New-Password Generates a 26 characters password. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [int] $Length = 26, [switch] $AsSecureString ) $lower = 97 .. 122 $upper = 65 .. 90 $special = '^', '~', '!', '@', '#', '$', '%', '^', '&', '*', '_', '+', '=', '`', '|', '\', '(', ')', '{', '}', '[', ']', ':', ';', '"', "'", '<', '>', ',', '.', '?', '/' $password = foreach ($number in (1 .. $Length)) { switch ($number % 3) { 0 { [char]($lower | Get-Random) } 1 { [char]($upper | Get-Random) } 2 { [char]($special | Get-Random) } } } if ($AsSecureString) { $password -join "" | ConvertTo-SecureString -AsPlainText -Force } else { $password -join "" } } function Reset-UserPassword { <# .SYNOPSIS Resets a user's password. .DESCRIPTION Resets a user's password. .PARAMETER Identity The user to reset. .PARAMETER Server The server to execute this against. .PARAMETER Password The password to apply. Defaults to a random password. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Reset-UserPassword -Identity 'krbtgt' Resets the password on the krbtgt account. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [string] $Identity, [string] $Server, [SecureString] $Password = (New-Password -AsSecureString), [switch] $EnableException ) begin { $parameters = @{ Identity = $Identity NewPassword = $Password ErrorAction = 'Stop' } if ($Server) { $parameters["Server"] = $Server } } process { try { Write-PSFMessage -String 'Reset-UserPassword.PerformingReset' -StringValues $Identity Set-ADAccountPassword @parameters Write-PSFMessage -String 'Reset-UserPassword.PerformingReset.Success' -StringValues $Identity } catch { Stop-PSFFunction -String 'Reset-UserPassword.FailedToReset' -StringValues $Identity -ErrorRecord $_ -Cmdlet $PSCmdlet return } } } function Get-KrbAccount { <# .SYNOPSIS Returns information on the Krbtgt Account. .DESCRIPTION Returns information on the Krbtgt Account. Includes information on the Kerberos ticket configuration. Tries to use the GroupPolicy module to figure out the Kerberos policy settings. .PARAMETER Server The domain controller to ask for the information. .PARAMETER Identity The account to target. Defaults to the krbtgt account, but can be used to apply to other accounts (eg: The krbtgt account for a RODC) .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Get-KrbAccount Returns the krbtgt account information. #> [CmdletBinding()] param ( [string] $Server, [string] $Identity = 'krbtgt', [switch] $EnableException ) begin { #region Prepare Preliminaries $parameter = @{ Identity = $Identity Properties = 'PasswordLastSet' } if ($Server) { $parameter['Server'] = $Server } try { if ($Server) { $domainName = (Get-ADDomain -Server $Server -ErrorAction Stop).DNSRoot } else { $domainName = (Get-ADDomain -ErrorAction Stop).DNSRoot } } catch { Stop-PSFFunction -String 'Get-KrbAccount.FailedDomainAccess' -ErrorRecord $_ -Cmdlet $PSCmdlet return } #endregion Prepare Preliminaries } process { if (Test-PSFFunctionInterrupt) { return } #region Get basic account properties Write-PSFMessage -String 'Get-KrbAccount.Start' -StringValues $Identity $krbtgt = Get-ADUser @parameter -ErrorAction Stop Write-PSFMessage -String 'Get-KrbAccount.UserFound' -StringValues $krbtgt.DistinguishedName -Level Debug $result = [PSCustomObject]@{ PSTypeName = 'Krbtgt.Account' EarliestResetTimestamp = $null Name = $krbtgt.Name SamAccountName = $krbtgt.SamAccountName DistinguishedName = $krbtgt.DistinguishedName PasswordLastSet = $krbtgt.PasswordLastSet MaxTgtLifetimeHours = 10 MaxClockSkewMinutes = 5 } #endregion Get basic account properties #region Retrieve Kerberos Policies try { Write-PSFMessage -String 'Get-KrbAccount.ScanningKerberosPolicy' -StringValues $domainName [xml]$gpo = Get-GPOReport -Guid '{31B2F340-016D-11D2-945F-00C04FB984F9}' -ReportType Xml -ErrorAction Stop -Domain $domainName $result.MaxTgtLifetimeHours = (($gpo.gpo.Computer.ExtensionData | Where-Object { $_.name -eq 'Security' }).Extension.ChildNodes | Where-Object { $_.Name -eq 'MaxTicketAge' }).SettingNumber $result.MaxClockSkewMinutes = (($gpo.gpo.Computer.ExtensionData | Where-Object { $_.name -eq 'Security' }).Extension.ChildNodes | Where-Object { $_.Name -eq 'MaxClockSkew' }).SettingNumber } catch { Write-PSFMessage -Level Warning -String 'Get-KrbAccount.FailedKerberosPolicyLookup' -StringValues $domainName -ErrorRecord $_ } #endregion Retrieve Kerberos Policies # This calculates the latest validity time time of existing krbtgt tickets from before the last reset might have. # Resetting the krbtgt password again before this expiry time risks preventing DCs from synchronizing the password on the second reset! $result.EarliestResetTimestamp = (($Krbtgt.PasswordLastSet.AddHours($result.MaxTgtLifetimeHours)).AddMinutes($result.MaxClockSkewMinutes)).AddMinutes($result.MaxClockSkewMinutes) Write-PSFMessage -String 'Get-KrbAccount.Success' -StringValues $result.SamAccountName, $result.EarliestResetTimestamp $result } } function Reset-KrbPassword { <# .SYNOPSIS Resets the krbtgt account's password. .DESCRIPTION Resets the krbtgt account's password. Performs test runs to ensure functionality. .PARAMETER DomainController An explicit list of domain controllers to manually replicate. Optional, defaults to all domain controllers. .PARAMETER PDCEmulator The PDCEmulator to work against. Will default against the local domain's PDC Emulator. The actual password reset is executed against this computer, all manual replication commands will replicate with this. .PARAMETER MaxDurationSeconds The maximum execution duration for the reset. Exceeding this duration will NOT interrupt the switch, but: - If exceeded during the test phase, the test will fail and the reset will be cancelled - If exceeded during execution, the overall result will be considered failed, even if technically a success. .PARAMETER DCSuccessPercent The percent of DCs that must successfully replicate the change in order to be considered a success. Defaults to 80% success rate. DC Replication commands are given by WinRM. .PARAMETER SkipTest Disables testing before execution. .PARAMETER Force By default, this command will refuse to reset the krbtgt account when there can still be a valid Kerberos ticket from before the last reset. Essentially, this means there is a cooldown after each krbtgt password reset. Using this parameter disables this barrier. DANGER: Using this parameter may lead to massive service interruption!!! Only use this in a case of utter desperation. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Reset-KrbPassword Resets the current domain's krbtgt account. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [PSFComputer[]] $DomainController, [PSFComputer] $PDCEmulator, [int] $MaxDurationSeconds = (Get-PSFConfigValue -FullName 'Krbtgt.Reset.MaxDurationSeconds' -Fallback 100), [int] $DCSuccessPercent = (Get-PSFConfigValue -FullName 'Krbtgt.Reset.DCSuccessPercent' -Fallback 100), [switch] $SkipTest, [switch] $Force, [switch] $EnableException ) begin { #region Resolve names & DCs to process Write-PSFMessage -String 'Reset-KrbPassword.DomainResolve' try { if ($PDCEmulator) { $pdcEmulatorInternal = $PDCEmulator } else { $pdcEmulatorInternal = (Get-ADDomain -ErrorAction Stop).PDCEmulator } $rwDomainControllers = Get-ADDomainController -Filter { IsReadOnly -eq $false } -Server $pdcEmulatorInternal -ErrorAction Stop | Where-Object { ($_.Name -ne $pdcEmulatorInternal.ComputerName) -and ("$($_.Name).$($_.Forest)" -ne $pdcEmulatorInternal.ComputerName) } if ($DomainController) { $rwDomainControllers = $rwDomainControllers | Where-Object ComputerName -in $DomainController } Write-PSFMessage -String 'Reset-KrbPassword.DomainResolve.Success' -StringValues $pdcEmulatorInternal } catch { Stop-PSFFunction -String 'Reset-KrbPassword.DomainResolve.Failed' -ErrorRecord $_ return } #endregion Resolve names & DCs to process } process { if (Test-PSFFunctionInterrupt) { return } $report = [PSCustomObject]@{ PSTypeName = 'Krbtgt.ResetResult' PDCEmulator = $pdcEmulatorInternal Account = $null Test = $null Reset = $null Sync = $null Success = $false Error = @() Start = $null End = $null Duration = $null } #region Access Information on the krbtgt account try { Write-PSFMessage -String 'Reset-KrbPassword.ReadKrbtgt' $report.Account = Get-KrbAccount -Server $pdcEmulatorInternal -EnableException Write-PSFMessage -String 'Reset-KrbPassword.ReadKrbtgt.Success' -StringValues $report.Account } catch { $report.Error += $_ Stop-PSFFunction -String 'Reset-KrbPassword.ReadKrbtgt.Failed' -ErrorRecord $_ -Cmdlet $PSCmdlet return $report } # Terminate if it is too soon to reset the password again if (-not $Force -and ($report.Account.EarliestResetTimestamp -gt (Get-Date))) { $report.Error += Write-Error "Cannot reset krbtgt password yet. Wait until $($report.Account.EarliestResetTimestamp) before trying again" -ErrorAction Continue 2>&1 Stop-PSFFunction -String 'Reset-KrbPassword.ReadKrbtgt.TooSoon' -StringValues $report.Account.EarliestResetTimestamp -Cmdlet $PSCmdlet -ErrorRecord $report.Error -OverrideExceptionMessage return $report } #endregion Access Information on the krbtgt account #region Perform tests if not disabled if (-not $SkipTest) { Write-PSFMessage -String 'Reset-KrbPassword.TestReset' $report.Test = Test-KrbPasswordReset -MaxDurationSeconds $MaxDurationSeconds -PDCEmulator $pdcEmulatorInternal -DomainController $rwDomainControllers -DCSuccessPercent $DCSuccessPercent if ($report.Test.Errors) { Write-PSFMessage -Level Warning -String 'Reset-KrbPassword.TestReset.ErrorCount' -StringValues ($report.Test.Errors | Measure-Object).Count foreach ($errorItem in $report.Test.Errors) { Write-PSFMessage -Level Warning -String 'Reset-KrbPassword.TestReset.ErrorItem' -StringValues $errorItem.Exception.Message $report.Error += $errorItem } } if (-not $report.Test.Success) { $report.Error = Write-Error "Test Reset Failed: $($report.Test.StatusCode)" 2>&1 Stop-PSFFunction -String 'Reset-KrbPassword.TestReset.Failed' -StringValues $report.Test.StatusCode -ErrorRecord $report.Error -Cmdlet $PSCmdlet return $report } } #endregion Perform tests if not disabled $report.Start = Get-Date #region Reset Krbtgt Password on PDC try { Write-PSFMessage -String 'Reset-KrbPassword.ActualReset' Reset-UserPassword -Server $pdcEmulatorInternal -Identity 'krbtgt' -EnableException Write-PSFMessage -String 'Reset-KrbPassword.ActualReset.Success' $report.Reset = $true } catch { $report.Reset = $false $report.Error += $_ Stop-PSFFunction -String 'Reset-KrbPassword.ActualReset.Failed' -ErrorRecord $_ -Cmdlet $PSCmdlet return $report } #endregion Reset Krbtgt Password on PDC #region Resync Domain Controllers Write-PSFMessage -String 'Reset-KrbPassword.SyncAccount' $report.Sync = Sync-KrbAccount -SourceDC $rwDomainControllers -TargetDC $pdcEmulatorInternal $report.End = Get-Date $report.Duration = $report.End - $report.Start Write-PSFMessage -String 'Reset-KrbPassword.ResetDuration' -StringValues $report.Duration $countSuccess = ($report.Sync | Where-Object Success | Measure-Object).Count $successPercent = $countSuccess / ($report.Sync | Measure-Object).Count * 100 if ($successPercent -lt $DCSuccessPercent) { Stop-PSFFunction -String 'Reset-KrbPassword.SyncAccount.FailedCount' -StringValues $successPercent, $DCSuccessPercent, (($report.Sync | Where-Object Success -eq $false).ComputerName -join ', ') return $report } if ($MaxDurationSeconds -lt $report.Duration.TotalSeconds) { Stop-PSFFunction -String 'Reset-KrbPassword.SyncAccount.FailedDuration' -StringValues $report.Duration, $MaxDurationSeconds -Cmdlet $PSCmdlet return $report } #endregion Resync Domain Controllers Write-PSFMessage -String 'Reset-KrbPassword.Success' $report.Success = $true return $report } } function Reset-KrbRODCPassword { <# .SYNOPSIS Reset the password on RODC krbtgt accounts. .DESCRIPTION Reset the password on RODC krbtgt accounts. .PARAMETER Name Name filter for what RODC to affect. .PARAMETER Server The directory server to initially work against. .PARAMETER Force By default, this command will refuse to reset the krbtgt account when there can still be a valid Kerberos ticket from before the last reset. Essentially, this means there is a cooldown after each krbtgt password reset. Using this parameter disables this barrier. DANGER: Using this parameter may lead to service interruption! Only use this in a case of utter desperation. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Reset-KrbRODCPassword Resets the password of all RODC krbtgt accounts. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [string] $Name = "*", [PSFComputer] $Server, [switch] $Force, [switch] $EnableException ) begin { #region Resolve names & DCs to process if ($Server) { $pdcEmulatorInternal = $Server } else { try { Write-PSFMessage -String 'Reset-KrbRODCPassword.ResolvePDC' $pdcEmulatorInternal = (Get-ADDomain).PDCEmulator } catch { Stop-PSFFunction -String 'Reset-KrbRODCPassword.ResolvePDC.Failed' -ErrorRecord $_ -Cmdlet $PSCmdlet return } } Write-PSFMessage -String 'Reset-KrbRODCPassword.ResolvePDC.Success' -StringValues $pdcEmulatorInternal #endregion Resolve names & DCs to process } process { if (Test-PSFFunctionInterrupt) { return } foreach ($rodc in (Get-RODomainController -Name $Name -Server $Server)) { $report = [PSCustomObject]@{ PSTypeName = 'Krbtgt.RODCResetResult' Server = $rodc.DnsHostName Account = $null Reset = $null Sync = $null Success = $false Error = @() Start = $null End = $null Duration = $null } #region Access Information on the krbtgt account try { Write-PSFMessage -String 'Reset-KrbRODCPassword.ReadKrbtgt' -StringValues $rodc.DnsHostName $report.Account = Get-KrbAccount -Server $pdcEmulatorInternal -Identity $rodc.KerberosAccount -EnableException Write-PSFMessage -String 'Reset-KrbRODCPassword.ReadKrbtgt.Success' -StringValues $report.Account } catch { $report.Error += $_ $report Stop-PSFFunction -String 'Reset-KrbRODCPassword.ReadKrbtgt.Failed' -StringValues $rodc.DnsHostName -ErrorRecord $_ -Cmdlet $PSCmdlet -Continue } # Terminate if it is too soon to reset the password again if (-not $Force -and ($report.Account.EarliestResetTimestamp -gt (Get-Date))) { $report.Error += Write-Error "Cannot reset krbtgt password for $($rodc.DnsHostName) yet. Wait until $($report.Account.EarliestResetTimestamp) before trying again" -ErrorAction Continue 2>&1 $report Stop-PSFFunction -String 'Reset-KrbRODCPassword.ReadKrbtgt.TooSoon' -StringValues $rodc.DnsHostName, $report.Account.EarliestResetTimestamp -Cmdlet $PSCmdlet -ErrorRecord $report.Error -Continue -OverrideExceptionMessage } #endregion Access Information on the krbtgt account $report.Start = Get-Date #region Reset Krbtgt Password on PDC try { Write-PSFMessage -String 'Reset-KrbRODCPassword.ActualReset' -StringValues $rodc.DnsHostName Reset-UserPassword -Server $rodc.ReplicationPartner[0] -Identity $rodc.KerberosAccount -EnableException Write-PSFMessage -String 'Reset-KrbRODCPassword.ActualReset.Success' -StringValues $rodc.DnsHostName $report.Reset = $true } catch { $report.Reset = $false $report.Error += $_ $report Stop-PSFFunction -String 'Reset-KrbRODCPassword.ActualReset.Failed' -StringValues $rodc.DnsHostName -ErrorRecord $_ -Cmdlet $PSCmdlet -Continue } #endregion Reset Krbtgt Password on PDC #region Resync Domain Controllers Write-PSFMessage -String 'Reset-KrbRODCPassword.SyncAccount' -StringValues $rodc.DnsHostName, $rodc.ReplicationPartner[0] $report.Sync = Sync-KrbAccount -SourceDC $rodc.DnsHostName -TargetDC $rodc.ReplicationPartner[0] $report.End = Get-Date $report.Duration = $report.End - $report.Start Write-PSFMessage -String 'Reset-KrbRODCPassword.ResetDuration' -StringValues $rodc.DnsHostName, $report.Duration if ($report.Sync | Where-Object Success -EQ $false) { $report Stop-PSFFunction -String 'Reset-KrbRODCPassword.SyncAccount.Failed' -StringValues $rodc.KerberosAccount, $rodc.DnsHostName, (($report.Sync | Where-Object Success -EQ $false).ComputerName -join ", ") -Cmdlet $PSCmdlet -Continue } #endregion Resync Domain Controllers Write-PSFMessage -String 'Reset-KrbRODCPassword.Success' -StringValues $rodc.DnsHostName $report.Success = $true $report } } } function Sync-KrbAccount { <# .SYNOPSIS Forces a single item AD Replication. .DESCRIPTION Will command the replication of an AD User object between two DCs. Uses PowerShell remoting against the source DC(s). .PARAMETER SourceDC The DC to start the synchronization command from. .PARAMETER TargetDC The DC to replicate with. .PARAMETER Identity The user identity to replicate. Defaults to krbtgt. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Sync-KrbAccount -SourceDC 'dc1' -TargetDC 'dc2' Replicates the krbtgt account between dc1 and dc2. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [PSFComputer[]] $SourceDC, [Parameter(Mandatory = $true)] [string] $TargetDC, [string] $Identity = 'krbtgt', [switch] $EnableException ) begin { try { $krbtgtDN = (Get-ADUser -Identity $Identity -Server $TargetDC -ErrorAction Stop).DistinguishedName } catch { Stop-PSFFunction -String 'Sync-KrbAccount.UserNotFound' -StringValues $Identity, $TargetDC -ErrorRecord $_ return } } process { if (Test-PSFFunctionInterrupt) { return } $errorVar = @() $pwdLastSet = [System.DateTime]::FromFileTimeUtc((Get-ADObject -Identity $krbtgtDN -Server $TargetDC -Properties PwdLastSet).PwdLastSet) Write-PSFMessage -String 'Sync-KrbAccount.Connecting' -StringValues ($SourceDC -join ', '), $krbtgtDN -Target $SourceDC Invoke-PSFCommand -ComputerName $SourceDC -ScriptBlock { param ( $TargetDC, $KrbtgtDN, $PwdLastSet ) $message = repadmin.exe /replsingleobj $env:COMPUTERNAME $TargetDC $KrbtgtDN *>&1 $result = 0 -eq $LASTEXITCODE # Verify the password change was properly synced $pwdLastSetLocal = [System.DateTime]::FromFileTimeUtc((Get-ADObject -Identity $KrbtgtDN -Server $env:COMPUTERNAME -Properties PwdLastSet).PwdLastSet) if ($pwdLastSetLocal -ne $PwdLastSet) { $result = $false } [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME Success = $result Message = ($message | Where-Object { $_ }) ExitCode = $LASTEXITCODE Error = $null } } -ArgumentList $TargetDC, $krbtgtDN, $pwdLastSet -ErrorVariable errorVar -ErrorAction SilentlyContinue | Select-PSFObject -KeepInputObject -TypeName 'Krbtgt.SyncResult' foreach ($errorObject in $errorVar) { Write-PSFMessage -Level Warning -Message 'Sync-KrbAccount.ConnectError' -StringValues $errorObject.TargetObject -ErrorRecord $errorObject [PSCustomObject]@{ PSTypeName = 'Krbtgt.SyncResult' ComputerName = $errorObject.TargetObject Success = $false Message = $errorObject.Exception.Message ExitCode = 1 Error = $errorObject } } } } function Test-KrbPasswordReset { <# .SYNOPSIS Tests the account reset and synchronization functionality. .DESCRIPTION Tests the account reset and synchronization functionality. This is a dry run of what Reset-KrbPassword would do, executed using a temporary user account. .PARAMETER PDCEmulator The PDC Emulator to operate against. .PARAMETER DomainController The domain controller to synchronize with. .PARAMETER MaxDurationSeconds The maximum number of seconds a switch may take before being considered a failure. Defaults to 180 seconds .PARAMETER DCSuccessPercent The percent of DCs that need to successfully finish execution in order for this test to be considered a success. Defaults to 80 percent .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Test-KrbPasswordReset -PDCEmulator 'dc1.domain.com' -DomainController 'dc2', 'dc3' Tests the account password reset using a dummy account and returns, whether the execution would have been successful. #> [CmdletBinding()] param ( [string] $PDCEmulator = (Get-ADDomain).PDCEmulator, [PSFComputer[]] $DomainController, [int] $MaxDurationSeconds = (Get-PSFConfigValue -FullName 'Krbtgt.Reset.MaxDurationSeconds' -Fallback 100), [int] $DCSuccessPercent = (Get-PSFConfigValue -FullName 'Krbtgt.Reset.DCSuccessPercent' -Fallback 100), [switch] $EnableException ) begin { #region Create a test account to test SO replication with try { $randomName = "krbtgt_test_$(Get-Random -Minimum 100 -Maximum 999)" Write-PSFMessage -String 'Test-KrbPasswordReset.CreatingCanary' -StringValues $randomName $canaryAccount = New-ADUser -Name $randomName -PassThru -Server $PDCEmulator -ErrorAction Stop } catch { Stop-PSFFunction -String 'Test-KrbPasswordReset.FailedCanaryCreation' -StringValues $randomName -ErrorRecord $_ return } #endregion Create a test account to test SO replication with #region Ensure Domain Controller parameter is filled if (Test-PSFParameterBinding -ParameterName 'DomainController') { try { $DomainController = (Get-ADDomainController -Server $PDCEmulator -Filter * -ErrorAction Stop).HostName | Where-Object { $_ -ne $PDCEmulator } } catch { throw } } #endregion Ensure Domain Controller parameter is filled } process { if (Test-PSFFunctionInterrupt) { return } $result = [PSCustomObject]@{ PSTypeName = 'Krbtgt.TestResult' PDCEmulator = $PDCEmulator Start = $null End = $null Duration = $null Reset = $false Sync = @() DCTotal = ($DomainController | Measure-Object).Count DCSuccess = 0 DCSuccessPercent = 0 DCFailed = @() Errors = @() Success = $true Status = '' RWDCs = $DomainController } $result.Start = Get-Date #region Test 1: Password Reset Write-PSFMessage -String 'Test-KrbPasswordReset.ResettingPassword' -StringValues $canaryAccount.DistinguishedName -Target $canaryAccount.DistinguishedName try { Reset-UserPassword -Server $PDCEmulator -Identity $canaryAccount.DistinguishedName -EnableException $result.Reset = $true } catch { Write-PSFMessage -Level Warning -String 'Test-KrbPasswordReset.ResettingPasswordFailed' -StringValues $canaryAccount.DistinguishedName -Target $canaryAccount.DistinguishedName -ErrorRecord $_ $result.Reset = $false $result.Errors += $_ $result.Success = $false $result.Status = $result.Status, 'ResetError' -join ", " } #endregion Test 1: Password Reset #region Test 2: Resync Domain Controllers Write-PSFMessage -String 'Test-KrbPasswordReset.SynchronizingCanary' -StringValues $canaryAccount.DistinguishedName -Target $canaryAccount.DistinguishedName $result.Sync = Sync-KrbAccount -SourceDC $DomainController -TargetDC $PDCEmulator -Identity $canaryAccount.DistinguishedName -EnableException:$false $result.End = Get-Date $result.Duration = $result.End - $result.Start $result.DCSuccess = $result.Sync | Where-Object Success $result.DCSuccessPercent = ($result.DCSuccess | Measure-Object).Count / $result.DCTotal * 100 $result.Sync.Error | ForEach-Object { if ($_) { $result.Errors += $_ } } if ($result.Duration.TotalSeconds -gt $MaxDurationSeconds) { $result.Success = $false $result.Status = $result.Status, 'TooSlowError' -join ", " } if ($result.DCSuccessPercent -lt $DCSuccessPercent) { $result.Success = $false $result.Status = $result.Status, 'SyncErrorRateError' -join ", " } Write-PSFMessage -String 'Test-KrbPasswordReset.Concluded' -StringValues $result.Success, $result.Status, $canaryAccount.DistinguishedName -Target $canaryAccount.DistinguishedName #endregion Test 2: Resync Domain Controllers $result } end { if (Test-PSFFunctionInterrupt) { return } # Remove the test account after finishing its work try { $canaryAccount | Remove-ADUser -Server $PDCEmulator -Confirm:$false -ErrorAction Stop } catch { Stop-PSFFunction -String 'Test-KrbPasswordReset.FailedCanaryCleanup' -StringValues $canaryAccount.DistinguishedName } } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'Krbtgt' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'Krbtgt' -Name 'Import.DoDotSource' -Value $true -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'Krbtgt' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." Set-PSFConfig -Module 'Krbtgt' -Name 'Reset.DCSuccessPercent' -Value 100 -Initialize -Validation 'integer' -Description 'The default minimum percentage of DCs that must successfully replicate a password reset request in order for the reset to be considered successful.' Set-PSFConfig -Module 'Krbtgt' -Name 'Reset.MaxDurationSeconds' -Value 180 -Initialize -Validation 'integer' -Description 'The default maximum replication duration (in seconds) in order for the reset to be considered successful.' <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'Krbtgt.ScriptBlockName' -Scriptblock { } #> Register-PSFTeppScriptblock -Name "Krbtgt.PDC" -ScriptBlock { Get-PSFTaskEngineCache -Module krbtgt -Name PDCs } Register-PSFTeppScriptblock -Name "Krbtgt.DC" -ScriptBlock { $dcs = Get-PSFTaskEngineCache -Module krbtgt -Name DCs if ($fakeBoundParameters.PDCEmulator) { $dcs[(Get-ADDomain -Server $fakeBoundParameters.PDCEmulator).DNSRoot] } elseif ($fakeBoundParameters.Server) { $dcs[(Get-ADDomain -Server $fakeBoundParameters.Server).DNSRoot] } else { $dcs[(Get-ADDomain).DNSRoot] } } Register-PSFTeppScriptblock -Name "Krbtgt.RODC" -ScriptBlock { $rodcs = Get-PSFTaskEngineCache -Module krbtgt -Name RODCs if ($fakeBoundParameters.PDCEmulator) { $rodcs[(Get-ADDomain -Server $fakeBoundParameters.PDCEmulator).DNSRoot] } elseif ($fakeBoundParameters.Server) { $rodcs[(Get-ADDomain -Server $fakeBoundParameters.Server).DNSRoot] } else { $rodcs[(Get-ADDomain).DNSRoot] } } Register-PSFTeppArgumentCompleter -Command Get-KrbAccount -Parameter Server -Name Krbtgt.PDC Register-PSFTeppArgumentCompleter -Command Reset-KrbPassword -Parameter PDCEmulator -Name Krbtgt.PDC Register-PSFTeppArgumentCompleter -Command Reset-KrbPassword -Parameter DomainController -Name Krbtgt.DC Register-PSFTeppArgumentCompleter -Command Reset-KrbRODCPassword -Parameter Server -Name Krbtgt.PDC Register-PSFTeppArgumentCompleter -Command Reset-KrbRODCPassword -Parameter Name -Name Krbtgt.RODC Register-PSFTeppArgumentCompleter -Command Sync-KrbAccount -Parameter SourceDC, TargetDC -Name Krbtgt.DC Register-PSFTeppArgumentCompleter -Command Test-KrbPasswordReset -Parameter PDCEmulator -Name Krbtgt.PDC Register-PSFTeppArgumentCompleter -Command Test-KrbPasswordReset -Parameter DomainController -Name Krbtgt.DC New-PSFLicense -Product 'Krbtgt' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-04-05") -Text @" Copyright (c) 2019 Friedrich Weinmann 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. "@ #endregion Load compiled code |