diff --git a/Documentation/PowerShell/New-ADDBRestoreFromMediaScript.md b/Documentation/PowerShell/New-ADDBRestoreFromMediaScript.md index fd2abc0..ef8b1e8 100644 --- a/Documentation/PowerShell/New-ADDBRestoreFromMediaScript.md +++ b/Documentation/PowerShell/New-ADDBRestoreFromMediaScript.md @@ -46,9 +46,11 @@ Follow these steps on the target server in order to restore the domain controlle 4. Copy the backup data to a local drive, e.g. C:\Backup. -5. Run the `New-ADDBRestoreFromMediaScript -DatabasePath 'C:\Backup\Active Directory\ntds.dit' | Invoke-Expression` command. +5. Run the `New-ADDBRestoreFromMediaScript -DatabasePath 'C:\Backup\Active Directory\ntds.dit' > C:\Backup\Restore-ADDomainController.ps1` command. -6. Sit back and watch the magic happen. Up to 3 reboots will follow and the entire process may take up to 20 minutes to finish. You should then end up with a fully functional domain controller. +6. Review the freshly generated PowerShell script and execute it. + +7. Sit back and watch the magic happen. Up to 3 reboots will follow and the entire process may take up to 20 minutes to finish. You should then end up with a fully functional domain controller. The script that is generated by the `New-ADDBRestoreFromMediaScript` cmdlet does the following actions: @@ -72,24 +74,26 @@ The script that is generated by the `New-ADDBRestoreFromMediaScript` cmdlet does ### Example 1 ```powershell -PS C:\> New-ADDBRestoreFromMediaScript -DatabasePath 'C:\IFM\Active Directory\ntds.dit' | Invoke-Expression +PS C:\> New-ADDBRestoreFromMediaScript -DatabasePath 'C:\IFM\Active Directory\ntds.dit' > C:\IFM\Restore.ps1 ``` -Restores a domain controller from a previously created IFM backup. +Generates a domain controller restoration script from a previously created IFM backup. The script can then be reviewed, modified if necessary, and executed manually. ### Example 2 ```powershell -PS C:\> New-ADDBRestoreFromMediaScript -DatabasePath 'C:\IFM\Active Directory\ntds.dit' -BootKey 610bc29e6f62ca7004e9872cd51a0116 -SysvolPath 'C:\IFM\SYSVOL' +PS C:\> New-ADDBRestoreFromMediaScript -DatabasePath 'C:\IFM\Active Directory\ntds.dit' -BootKey 610bc29e6f62ca7004e9872cd51a0116 -SysvolPath 'C:\IFM\SYSVOL' > C:\IFM\Restore.ps1 ``` -Generates a domain controller restoration script from a previously created IFM backup. The script can then be reviewed, modified if necessary and executed manually. +Same as the previous example, but with explicitly provided SYSVOL directory path and boot key. ### Example 3 ```cmd -ntdsutil.exe "activate instance ntds" ifm "create sysvol full c:\IFM" quit quit +ntdsutil.exe "activate instance ntds" ifm "create sysvol full C:\IFM" quit quit +icacls.exe C:\Windows\Sysvol\domain\Policies\* /save C:\IFM\SYSVOL\PolicyPermissions.txt ``` -Creates an Install From Media (IFM) backup of a running domain controller. This backup can later be used by the New-ADDBRestoreFromMediaScript cmdlet. +Creates an Install From Media (IFM) backup of a running domain controller and exports Group Policy ACLs. +This backup can later be used by the New-ADDBRestoreFromMediaScript cmdlet. ### Example 4 This is a sample PowerShell script generated by the New-ADDBRestoreFromMediaScript cmdlet: @@ -97,166 +101,527 @@ This is a sample PowerShell script generated by the New-ADDBRestoreFromMediaScri ```powershell <# .SYNOPSIS -Restores the LON-DC1 domain controller from ntds.dit. +Restores the LON-DC1 domain controller from its ntds.dit file. + +.DESCRIPTION + +This script performs a multi-phase domain controller restore from an IFM backup: +- Phase 0: Initiate the restore process and create a VSS backup. +- Phase 1: Set the local Administrator password if empty. +- Phase 2: Rename the computer and reboot if necessary. +- Phase 3: Install the required Windows features and reboot if needed. +- Phase 4: Promote the server to a domain controller. +- Phase 5: Restore the AD database, re-encrypt it, reset the DC password, and reconfigure LSA policies. +- Phase 6: Replace the SYSVOL directory, restore ACLs if available, and reboot the server. +- Phase 7: Reconfigure the SYSVOL replication subscription. + +Script exection logs can be found in the C:\Windows\Logs\DSInternals-RestoreFromMedia.txt file. -.REMARKS +.PARAMETER Phase +Specifies the phase of the restore operation to execute. Used to orchestrate the integrated recovery workflow. + +.NOTES This script should only be executed on a freshly installed Windows Server 2012 R2 Datacenter Evaluation. Use at your own risk. The DSInternals PowerShell module must be installed for all users on the target server. +It is recommended to change the DSRM password after DC promotion. - -Author: Michael Grafnetter +Author: Michael Grafnetter +Version: 2.0 #> -#Requires -Version 3 -Modules DSInternals,ServerManager,PSScheduledJob -RunAsAdministrator -# Perform a VSS backup before doing anything else. -Write-Host 'Creating a snapshot of the system drive to make rollback possible...' -$vssResult = ([wmiclass] 'Win32_ShadowCopy').Create("$env:SystemDrive\", 'ClientAccessible') +#Requires -Version 3 -Modules DSInternals,ServerManager -RunAsAdministrator -# The PS module must be present as workflows cannot contain non-existing activities. -Write-Host 'Installing the Active Directory module for Windows PowerShell...' -Add-WindowsFeature -Name RSAT-AD-PowerShell -ErrorAction Stop +param( + [Parameter(Mandatory = $false)] + [ValidateRange(0, 7)] + [int] $Phase = 0 +) -# All the other operations will be executed by a restartable workflow running in SYSTEM context. -Write-Host 'Registering restartable workflows...' +# Make sure that the required data types and cmdlets are available. +Import-Module -Name DSInternals -ErrorAction Stop +Import-Module -Name ServerManager -ErrorAction Stop -# Delete any pre-existing scheduled jobs with the same names before registering new ones. -Unregister-ScheduledJob -Name DSInternals-RFM-Initializer,DSInternals-RFM-Resumer -Force -ErrorAction SilentlyContinue +function Main { + [string] $script:LogFile = "$env:windir\Logs\DSInternals-RestoreFromMedia.txt" + Write-Log -Message "Starting script execution in phase $Phase..." -# The DSInternals-RFM-Initializer job will only be executed once in order to register the workflow and to invoke it for the first time. -$initTask = Register-ScheduledJob -Name DSInternals-RFM-Initializer -ScriptBlock { - workflow Restore-DomainController + # The script must be executed locally so that it is accessible even after a reboot. + Test-ScriptPath + + switch($script:Phase) { - if ($env:COMPUTERNAME -ne 'LON-DC1') - { - # A server rename operation is required. - Rename-Computer -NewName 'LON-DC1' -Force - - # We explicitly suspend the workflow as Restart-Computer with the -Wait parameter does not survive local reboots. - shutdown.exe /r /t 5 - Suspend-Workflow -Label 'Waiting for reboot' - } + 0 { + Write-Log 'The LON-DC1 domain controller will now be restored from media. Up to 3 reboots will follow shortly.' - if ((Get-Service NTDS -ErrorAction SilentlyContinue) -eq $null) - { - # A DC promotion is required. - # Note: In order to maintain compatibility with Windows Server 2008 R2, the ADDSDeployment module is not used. - # Advice: It is recommenced to change the DSRM password after DC promotion. - dcpromo.exe /unattend /ReplicaOrNewDomain:Domain /NewDomain:Forest /NewDomainDNSName:"adatum.com" /DomainNetBiosName:"ADATUM" /DomainLevel:7 /ForestLevel:7 '/SafeModeAdminPassword:"Pa$$w0rd"' /DatabasePath:"$env:SYSTEMROOT\NTDS" /LogPath:"$env:SYSTEMROOT\NTDS" /SysVolPath:"$env:SYSTEMROOT\SYSVOL" /AllowDomainReinstall:Yes /CreateDNSDelegation:No /DNSOnNetwork:No /InstallDNS:Yes /RebootOnCompletion:No - Set-ItemProperty -Path registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ServerManager\Roles\10 -Name ConfigurationStatus -Value 2 -Force - - <# Alternative approach for Winows Server 2012+ - Install-WindowsFeature -Name AD-Domain-Services - Install-ADDSForest -DomainName 'adatum.com' ` - -DomainNetbiosName 'ADATUM' ` - -ForestMode WinThreshold ` - -DomainMode WinThreshold ` - -DatabasePath "$env:SYSTEMROOT\NTDS" ` - -LogPath "$env:SYSTEMROOT\NTDS" ` - -SysvolPath "$env:SYSTEMROOT\SYSVOL" ` - -InstallDns ` - -CreateDnsDelegation:$false ` - -NoDnsOnNetwork ` - -SafeModeAdministratorPassword (ConvertTo-SecureString -String 'Pa$$w0rd' -AsPlainText -Force)` - -NoRebootOnCompletion ` - -Force - #> + # Perform a VSS backup before doing anything else. + New-VolumeShadowCopy -Volume $env:SystemDrive + + # Invoke the first phase in the background. + Register-ScheduledScript -ExecutePhase 1 } + 1 { + # The local Administrator account must have a password set for dcpromo to succeed. + Reset-LocalAdministratorPassword -NewPassword 'Pa$$w0rd' - # Reboot the computer into the Directory Services Restore Mode. - bcdedit.exe /set safeboot dsrepair - shutdown.exe /r /t 5 - Suspend-Workflow -Label 'Waiting for reboot' - - # Re-encrypt the DB with the new boot key. - $currentBootKey = Get-BootKey -Online - Set-ADDBBootKey -DatabasePath 'C:\Backup\Active Directory\ntds.dit' -LogPath 'C:\Backup\Active Directory' -OldBootKey 610bc29e6f62ca7004e9872cd51a0116 -NewBootKey $currentBootKey -Force - - # Clone the DC account password. - $ntdsParams = Get-ItemProperty -Path registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters - InlineScript { - # Note: SupplementalCredentials do not get serialized properly without using the InlineScript activity. - $dcAccount = Get-ADDBAccount -SamAccountName 'LON-DC1$' -DatabasePath $using:ntdsParams.'DSA Database file' -LogPath $using:ntdsParams.'Database log files path' -BootKey $using:currentBootKey - Set-ADDBAccountPasswordHash -ObjectGuid 9bb4d6f4-060a-4585-9f18-625774e7c088 -NTHash $dcAccount.NTHash -SupplementalCredentials $dcAccount.SupplementalCredentials -DatabasePath 'C:\Backup\Active Directory\ntds.dit' -LogPath 'C:\Backup\Active Directory' -BootKey $using:currentBootKey -Force + # Continue to the next phase. + Register-ScheduledScript -ExecutePhase 2 + } + 2 { + Write-Log -Message 'Checking the computer name...' + [bool] $computerRenameRequired = $env:COMPUTERNAME -ne 'LON-DC1' + + if ($computerRenameRequired) { + # A server rename operation is required. + # Note: The host name will automatically be truncated to 15 characters. + Write-Log -Message 'Renaming the computer to LON-DC1...' + Rename-Computer -NewName 'LON-DC1' -Force -Verbose *>> $script:LogFile + } else { + Write-Log -Message 'The local system already has the correct name. Skipping the rename operation.' + } + + # Perform an optional reboot and continue to the next phase. + Register-ScheduledScript -ExecutePhase 3 -RebootRequired:$computerRenameRequired + } + 3 { + Write-Log -Message 'Installing the required Windows features...' + + # Note: dcpromo.exe would install most of these features if absent. + # TODO: Check if these features are called the same in all OS versions. + [string[]] $featuresToInstall = @( + 'AD-Domain-Services', + 'DNS', + 'GPMC', + 'RSAT-ADDS', + 'RSAT-AD-AdminCenter', + 'RSAT-ADDS-Tools', + 'RSAT-AD-PowerShell', + 'RSAT-DNS-Server', + 'RSAT-DFS-Mgmt-Con', # dfsrdiag.exe is not installed by default + 'RSAT-Feature-Tools-BitLocker-BdeAducExt' # BitLocker Recovery Password Viewer is not installed by default + ) + + [object[]] $featuresRequiringRestart = Get-WindowsFeature | + Where-Object Name -in $featuresToInstall | + Install-WindowsFeature -IncludeManagementTools -IncludeAllSubFeature | + Where-Object RestartNeeded -ne ([Microsoft.Windows.ServerManager.Commands.RestartState]::No) 2>> $script:LogFile + + [bool] $restartNeeded = $null -ne $featuresRequiringRestart + + # Perform an optional reboot and continue to the next phase. + Register-ScheduledScript -ExecutePhase 4 -RebootRequired:$restartNeeded + } + 4 { + # Check if the NTDS service is present and enabled, possibly by a previous script execution. + Write-Log -Message 'Checking the state of the NTDS service...' + [System.ServiceProcess.ServiceController] $ntdsService = Get-Service -Name NTDS -ErrorAction SilentlyContinue + + if ($null -eq $ntdsService -or $ntdsService.StartType -eq [System.ServiceProcess.ServiceStartMode]::Disabled) { + # A DC promotion is required. + Write-Log -Message 'Promoting the server to a domain controller...' + + # Note: In order to maintain compatibility with Windows Server 2008 R2, the ADDSDeployment PS module is not used. + dcpromo.exe /unattend /ReplicaOrNewDomain:Domain /NewDomain:Forest /NewDomainDNSName:"adatum.com" /DomainNetBiosName:"ADATUM" /DomainLevel:7 /ForestLevel:7 '/SafeModeAdminPassword:"Pa$$w0rd"' /DatabasePath:"$env:SYSTEMROOT\NTDS" /LogPath:"$env:SYSTEMROOT\NTDS" /SysVolPath:"$env:SYSTEMROOT\SYSVOL" /AllowDomainReinstall:Yes /CreateDNSDelegation:No /DNSOnNetwork:No /InstallDNS:Yes /RebootOnCompletion:No *>> $script:LogFile + } else { + Write-Log -Message 'The server is already a domain controller. Skipping dcpromo execution.' + } + + # Prevent the Server Manager from saying that additional configuration is required. + # TODO: Test the following registry key on Windows Server 2008 R2. + Write-Log -Message 'Marking the AD DS role as already configured by the Server Manager...' + Set-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ServerManager\Roles\10' ` + -Name 'ConfigurationStatus' ` + -Value 2 ` + -Type DWord ` + -Force ` + -ErrorAction Continue ` + -Verbose *>> $script:LogFile + + # Avoid FSMO role holder being unavailable until it has completed replication of a writeable directory partition. + Write-Log -Message 'Disabling the initial datatabse synchronization...' + Set-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\NTDS\Parameters' ` + -Name 'Repl Perform Initial Synchronizations' ` + -Value 0 ` + -Type DWord ` + -Force ` + -Verbose *>> $script:LogFile + + # Continue with post-installation tasks. + Register-ScheduledScript -ExecutePhase 5 } + 5 { + # Make sure that AD DS is not running. + Write-Log -Message 'Checking the state of the NTDS service...' + [System.ServiceProcess.ServiceController] $ntdsService = Get-Service -Name NTDS -ErrorAction SilentlyContinue + + if($null -eq $ntdsService) { + Write-Log -Message 'Could not find the NTDS service. Terminating...' + break + } elseif ($ntdsService.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Running) { + Write-Log -Message 'Stopping the NTDS service...' + Stop-Service -Name NTDS -Force -Verbose *>> $script:LogFile + } else { + # Note: This is the most common case, as AD DS should not be running after a fresh dcpromo. + Write-Log -Message 'The NTDS service is stopped. Proceeding with database restoration...' + } + + # Re-encrypt the DB with the new boot key. + Write-Log -Message 'Fetching the current boot key (system key) from the HKLM\SYSTEM registry hive...' + [byte[]] $currentBootKey = Get-BootKey -Online 2>> $script:LogFile + + Write-Log -Message 'Re-encrypting the database with the new boot key...' + Set-ADDBBootKey -DatabasePath 'C:\Backup\Active Directory\ntds.dit' ` + -LogPath 'C:\Backup\Active Directory' ` + -OldBootKey '610bc29e6f62ca7004e9872cd51a0116' ` + -NewBootKey $currentBootKey ` + -ErrorAction Stop ` + -Force ` + -Verbose *>> $script:LogFile + + # Fetch the DB and transaction log locations. + Write-Log -Message 'Fetching NTDS service parameters from the registry...' + [PSCustomObject] $ntdsParams = Get-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' 2>> $script:LogFile + + # Fetch the DC account password from the newly created DC. + Write-Log -Message 'Fetching the new DC account password hashes...' + [DSInternals.Common.Data.DSAccount] $dcAccount = Get-ADDBAccount -SamAccountName 'LON-DC1$' ` + -DatabasePath $ntdsParams.'DSA Database file' ` + -LogPath $ntdsParams.'Database log files path' ` + -BootKey $currentBootKey ` + -ErrorAction Stop 2>> $script:LogFile + + # Clone the DC account password to the database that is being restored. + Write-Log -Message 'Cloning the DC account password hashes...' + Set-ADDBAccountPasswordHash -ObjectGuid '9bb4d6f4-060a-4585-9f18-625774e7c088' ` + -NTHash $dcAccount.NTHash ` + -SupplementalCredentials $dcAccount.SupplementalCredentials ` + -DatabasePath 'C:\Backup\Active Directory\ntds.dit' ` + -LogPath 'C:\Backup\Active Directory' ` + -BootKey $currentBootKey ` + -Verbose ` + -Force *>> $script:LogFile + + # Replace the database files using robocopy. + # Copy the database (*.dit, *.edb), checkpoint (*.chk), and flush map (*.jfm) files. + # /MIR: Mirrors the directory tree + # /NP: No progress + # /NDL: No directory list + # /NJS: No job summary + Write-Log -Message 'Replacing the AD database files...' + robocopy.exe 'C:\Backup\Active Directory' $ntdsParams.'DSA Working Directory' *.dit *.edb *.chk *.jfm /MIR /NP /NDL /NJS *>> $script:LogFile + + # Replace the transaction logs using robocopy. + # Copy the transaction logs (*.log) and reserved transaction log files (*.jrs). + # /MIR: Mirrors the directory tree + # /NP: No progress + # /NDL: No directory list + # /NJS: No job summary + Write-Log -Message 'Replacing the AD database transaction log files...' + robocopy.exe 'C:\Backup\Active Directory' $ntdsParams.'Database log files path' *.log *.jrs /MIR /NP /NDL /NJS *>> $script:LogFile + + # Reconfigure LSA policies. We would get into a BSOD loop if they do not match the corresponding values in the database. + Write-Log -Message 'Reconfiguring the LSA policies...' + Set-LsaPolicyInformation -DomainName 'ADATUM' ` + -DnsDomainName 'Adatum.com' ` + -DnsForestName 'Adatum.com' ` + -DomainGuid '279b615e-ae79-4c86-a61a-50f687b9f7b8' ` + -DomainSid 'S-1-5-21-1817670852-3242289776-1304069626' ` + -Verbose *>> $script:LogFile + + # Tell the DC that its DB has intentionally been restored. A new InvocationID will be generated as soon as the service starts. + Write-Log -Message 'Marking the database as restored from backup...' + Set-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' ` + -Name 'Database restored from backup' ` + -Value 1 ` + -Type DWord ` + -Force ` + -Verbose *>> $script:LogFile + + # Remove the DSA Database Epoch value to bypass database rollback detection. + Write-Log -Message 'Clearing the DSA Database Epoch value...' + Remove-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' ` + -Name 'DSA Database Epoch' ` + -Force ` + -Verbose *>> $script:LogFile + + # Reboot and continue to the next phase. + Register-ScheduledScript -ExecutePhase 6 -RebootRequired + } + 6 { + # Replace the SYSVOL directory. + # /MIR: Mirrors the directory tree + # /XD: Excludes the DfsrPrivate directory from being copied. + # /XJ: Excludes junction points, several of which are present in SYSVOL. + # /COPYALL: Copies all file information, including data, attributes, timestamps, NTFS ACLs (permissions), owner information, and auditing information. + # /SECFIX: Fixes file security on all files, even skipped ones. + # /TIMFIX: This fixes file times on all files, even skipped ones. + # /NP: No progress + # /NDL: No directory list + Write-Log -Message 'Replacing the SYSVOL files...' + [string] $sourcePath = Join-Path -Path 'C:\Backup\SYSVOL' -ChildPath 'Adatum.com' + [string] $targetPath = Join-Path -Path "$env:SYSTEMROOT\SYSVOL" -ChildPath 'domain' + robocopy.exe $sourcePath $targetPath /MIR /XD DfsrPrivate /XJ /COPYALL /SECFIX /TIMFIX /NP /NDL *>> $script:LogFile + + # Check if an optional SYSVOL Group Policy ACL backup is present. + # This would be useful in situations where the SYSVOL is backed up to a non-NTFS file system. + [string] $sysvolAclBackupPath = Join-Path -Path 'C:\Backup\SYSVOL' -ChildPath 'PolicyPermissions.txt' + if(Test-Path -Path $sysvolAclBackupPath -PathType Leaf) { + Write-Log -Message 'Restoring the SYSVOL Group Policy ACLs...' + [string] $aclRestorePath = Join-Path -Path "$env:SYSTEMROOT\SYSVOL" -ChildPath 'domain\Policies' + icacls.exe $aclRestorePath /restore $sysvolAclBackupPath *>> $script:LogFile + } else { + Write-Log -Message 'No SYSVOL ACL backup found. Skipping the ACL restoration.' + } + + # A reboot is required for AD to start normally. + Register-ScheduledScript -ExecutePhase 7 -RebootRequired + } + 7 { + # Reconfigure SYSVOL replication in case it has been restored to a different path. + + # Make sure that AD Web Services are available. + [System.ServiceProcess.ServiceController] $adws = Get-Service -Name NTDS -ErrorAction SilentlyContinue + + if($null -eq $adws -or $adws.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Running) { + Write-Log -Message 'AD Web Services are not available. Terminating...' + break + } else { + Write-Log -Message 'The AD Web Services service is running. Proceeding with SYSVOL subscription reconfiguration...' + } + + # Update DFS-R subscription if present in AD. + Update-DfsrSubscription -DomainControllerDN 'CN=LON-DC1,OU=Domain Controllers,DC=Adatum,DC=com' ` + -SysvolPath "$env:SYSTEMROOT\SYSVOL" ` + -DomainName 'Adatum.com' + + # Update FRS subscription if present in AD. + Update-FrsSubscription -DomainControllerDN 'CN=LON-DC1,OU=Domain Controllers,DC=Adatum,DC=com' ` + -SysvolPath "$env:SYSTEMROOT\SYSVOL" + } + } - # Replace the database and transaction logs. - robocopy.exe 'C:\Backup\Active Directory' $ntdsParams.'DSA Working Directory' *.dit *.edb *.chk *.jfm /MIR /NP /NDL /NJS - robocopy.exe 'C:\Backup\Active Directory' $ntdsParams.'Database log files path' *.log *.jrs /MIR /NP /NDL /NJS + if($Phase -ge 1) { + [string] $taskName = "DSInternals-RFM-Phase$Phase" + Write-Log -Message "Removing the scheduled task $taskName..." + schtasks.exe /Delete /TN $taskName /F *>> $script:LogFile + } - # Replace SYSVOL. - robocopy.exe 'C:\Backup\SYSVOL\Adatum.com' "$env:SYSTEMROOT\SYSVOL\domain" /MIR /XD DfsrPrivate /XJ /COPYALL /SECFIX /TIMFIX /NP /NDL + Write-Log -Message "Execution of phase $Phase has finished." +} - # Reconfigure LSA policies. We would get into a BSOD loop if they do not match the corresponding values in the database. - Set-LsaPolicyInformation -DomainName 'ADATUM' -DnsDomainName 'Adatum.com' -DnsForestName 'Adatum.com' -DomainGuid 279b615e-ae79-4c86-a61a-50f687b9f7b8 -DomainSid S-1-5-21-1817670852-3242289776-1304069626 +#region Helper Functions - # Tell the DC that its DB has intentionally been restored. A new InvocationID will be generated as soon as the service starts. - Set-ItemProperty -Path registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters -Name 'Database restored from backup' -Value 1 -Force - Remove-ItemProperty -Path registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters -Name 'DSA Database Epoch' -Force +<# +.SYNOPSIS +Resets the password of the local Administrator account (RID=500) +if it has not been set yet. - # Disable DSRM and do one last reboot. - bcdedit.exe /deletevalue safeboot - shutdown.exe /r /t 5 - Suspend-Workflow -Label 'Waiting for reboot' +.NOTES +This recovery script intentionally contains plaintext passwords. +Using a SecureString in this function would not provide any additional security. +The corresponding PSScriptAnalyzer warning is therefore suppressed. - # Reconfigure SYSVOL replication in case it has been restored to a different path. +#> +function Reset-LocalAdministratorPassword { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] + param( + [Parameter(Mandatory = $true)] + [string] $NewPassword + ) + + # Note: Due to compatibility reasons, the Get-LocalUser cmdlet is not used. + Write-Log -Message 'Fetching built-in Administrator account information through WMI...' + [wmi] $builtinAdminWMI = Get-WmiObject -Class Win32_UserAccount -Filter 'SID LIKE "%-500"' -Property Name + + Write-Log -Message 'Fetching built-in Administrator account information through ADSI...' + [adsi] $builtinAdminADSI = 'WinNT://./{0},User' -f $builtinAdminWMI.Name + [bool] $hasPassword = $builtinAdminADSI.PasswordAge.Value -gt 0 + + if(-not $hasPassword) { + Write-Log 'Setting a password for the local Administrator account...' + $builtinAdminWMI.SetPassword($NewPassword) *>> $script:LogFile + } else { + Write-Log 'A password for the local Administrator account has already been set. No action is required.' + } +} - # Update DFS-R subscription if present in AD. - $dfsrSubscriptionDN = 'CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,CN=LON-DC1,OU=Domain Controllers,DC=Adatum,DC=com' - $dfsrSubscription = Set-ADObject -Identity $dfsrSubscriptionDN -Server localhost -PassThru -ErrorAction SilentlyContinue -Replace @{ - 'msDFSR-RootPath' = "$env:SYSTEMROOT\SYSVOL\domain"; - 'msDFSR-StagingPath' = "$env:SYSTEMROOT\SYSVOL\staging areas\Adatum.com" - } +<# +.SYNOPSIS +Creates a new volume shadow copy of the specified volume. +#> +function New-VolumeShadowCopy { + param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^[A-Z]:$')] + [string] $Volume + ) + + Write-Log 'Creating a snapshot of the system drive to make rollback possible...' + ([wmiclass] 'Win32_ShadowCopy').Create("$Volume\", 'ClientAccessible') *>> $script:LogFile +} - if($dfsrSubscription -ne $null) - { - # Download the updated DFS-R configuration from AD. - Invoke-WmiMethod -Class DfsrConfig -Name PollDsNow -ArgumentList localhost -Namespace ROOT\MicrosoftDfs - } +function Register-ScheduledScript { + param( + [Parameter(Mandatory = $true)] + [int] $ExecutePhase, - # Update FRS subscription if present in AD. - $frsSubscriptionDN = 'CN=Domain System Volume (SYSVOL share),CN=NTFRS Subscriptions,CN=LON-DC1,OU=Domain Controllers,DC=Adatum,DC=com' - $frsSubscription = Set-ADObject -Identity $frsSubscriptionDN -Server localhost -PassThru -ErrorAction SilentlyContinue -Replace @{ - 'fRSRootPath' = "$env:SYSTEMROOT\SYSVOL\domain"; - 'fRSStagingPath' = "$env:SYSTEMROOT\SYSVOL\staging\domain" - } + [Parameter(Mandatory = $false)] + [switch] $RebootRequired + ) + + # Each phase of the script is executed by a separate scheduled task. + [string] $taskName = "DSInternals-RFM-Phase$ExecutePhase" - if($frsSubscription -ne $null) - { - # Download the updated FRS configuration from AD. - InlineScript { ntfrsutl.exe poll /now } + # Locate powershell.exe + [string] $psPath = Get-Command -Name 'powershell.exe' -CommandType Application | Select-Object -ExpandProperty Path + + # Locate the current PS script + [string] $scriptPath = $script:MyInvocation.MyCommand.Source + + # Generate the complete command line + [string] $commandLine = '"{0}" -ExecutionPolicy Bypass -NonInteractive -NoProfile -NoLogo -File "{1}" -Phase {2}' -f $psPath,$scriptPath,$ExecutePhase + + # NOTE: The ScheduledTasks module is not used because it is not available on Windows Server 2008 R2. + Write-Log -Message 'Registering the script to be executed as a scheduled task...' + schtasks.exe /Create /TN $taskName /TR $commandLine /SC ONSTART /RU SYSTEM /RL HIGHEST /F *>> $script:LogFile + + if($RebootRequired) { + # Reboot the computer and let it automatically execute the scheduled task. + Write-Log -Message 'Rebooting the computer to continue the restore process...' + shutdown.exe /r /t 5 /f *>> $script:LogFile + } else { + # Start the scheduled task immediately. + Write-Log -Message 'Starting the scheduled task...' + schtasks.exe /Run /I /TN $taskName *>> $script:LogFile + } +} + +<# +.SYNOPSIS +Checks whether the script is being executed from a local file. +#> +function Test-ScriptPath { + Write-Log -Message 'Checking if the script is being executed from a local file...' + + # Check if the PSScriptRoot variable is set. + [bool] $isScript = -not [string]::IsNullOrEmpty($PSScriptRoot) + + if(-not $isScript) { + Write-Log -Message 'Not running as script. Terminating...' + throw 'The script must be executed from a local file.' + } else { + if(-not ([uri] $PSScriptRoot).IsUnc) { + Write-Log -Message 'The script is being executed from a local file.' + } else { + Write-Log -Message 'Running from a UNC path. Terminating...' + throw 'The script must be executed from a local file.' } } +} + +<# +.SYNOPSIS +Updates the DFS-R subscription if present in AD. + +.PARAMETER DomainControllerDN +The distinguished name of the domain controller computer account. - # Delete any pre-existing workflows with the same name before starting a new one. - Remove-Job -Name DSInternals-RFM-Workflow -Force -ErrorAction SilentlyContinue +.PARAMETER SysvolPath +The path to the SYSVOL share on the target domain controller. +#> +function Update-DfsrSubscription { + param( + [Parameter(Mandatory = $true)] + [string] $DomainControllerDN, + + [Parameter(Mandatory = $true)] + [string] $SysvolPath, + + [Parameter(Mandatory = $true)] + [string] $DomainName + ) + + # Make sure that the required data types and cmdlets are available. + Import-Module -Name ActiveDirectory -ErrorAction Stop + + Write-Log -Message 'Updating the FRS subscription object in AD...' + [string] $dfsrSubscriptionDN = "CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,$DomainControllerDN" + [Microsoft.ActiveDirectory.Management.ADObject] $dfsrSubscription = Set-ADObject -Identity $dfsrSubscriptionDN -Server localhost -PassThru -ErrorAction SilentlyContinue -Replace @{ + 'msDFSR-RootPath' = Join-Path -Path $SysvolPath -ChildPath 'domain' + 'msDFSR-StagingPath' = Join-Path -Path $SysvolPath -ChildPath "staging areas\$DomainName" + } - # Start the workflow. - Restore-DomainController -JobName DSInternals-RFM-Workflow + if($null -ne $dfsrSubscription) { + # Download the updated DFS-R configuration from AD. + Write-Log -Message 'Polling AD for DFS-R configuration changes...' + Invoke-WmiMethod -Class DfsrConfig -Name PollDsNow -ArgumentList localhost -Namespace ROOT\MicrosoftDfs *>> $script:LogFile + } else { + Write-Log -Message 'DFS-R subscription was not found in AD. Has the domain not yet been migrated from FRS?' + } } -# The DSInternals-RFM-Resumer job will be executed after each reboot to unpause the workflow. -$resumeTask = Register-ScheduledJob -Name DSInternals-RFM-Resumer -Trigger (New-JobTrigger -AtStartup) -ScriptBlock { - # Unregister the one-time task that must already have been executed. - Unregister-ScheduledJob -Name DSInternals-RFM-Initializer -Force -ErrorAction SilentlyContinue +<# +.SYNOPSIS +Updates the FRS subscription if present in AD. + +.PARAMETER DomainControllerDN +The distinguished name of the domain controller computer account. - # Resume the workflow after the computer is rebooted. - Resume-Job -Name DSInternals-RFM-Workflow -Wait | Wait-Job | where State -In Completed,Failed,Stopped | foreach { - # Perform cleanup when finished. - Remove-Job -Job $PSItem -Force - Unregister-ScheduledJob -Name DSInternals-RFM-Resumer -Force +.PARAMETER SysvolPath +The path to the SYSVOL share on the target domain controller. +#> +function Update-FrsSubscription { + param( + [Parameter(Mandatory = $true)] + [string] $DomainControllerDN, + + [Parameter(Mandatory = $true)] + [string] $SysvolPath + ) + + # Make sure that the required data types and cmdlets are available. + Import-Module -Name ActiveDirectory -ErrorAction Stop + + # Update FRS subscription if present in AD. + Write-Log -Message 'Updating the FRS subscription object in AD...' + [string] $frsSubscriptionDN = "CN=Domain System Volume (SYSVOL share),CN=NTFRS Subscriptions,$DomainControllerDN" + [Microsoft.ActiveDirectory.Management.ADObject] $frsSubscription = Set-ADObject -Identity $frsSubscriptionDN -Server localhost -PassThru -Verbose -ErrorAction SilentlyContinue -Replace @{ + 'fRSRootPath' = Join-Path -Path $SysvolPath -ChildPath 'domain' + 'fRSStagingPath' = Join-Path -Path $SysvolPath -ChildPath 'staging\domain' } + + if($null -ne $frsSubscription) { + # Download the updated FRS configuration from AD. + Write-Log -Message 'Polling AD for FRS configuration changes...' + ntfrsutl.exe poll /now *>> $script:LogFile + # TODO: Check what happens if the FRS service is disabled on the new DC. + } else { + Write-Log -Message 'FRS subscription was not found in AD. This is expected.' + } +} + +<# +.SYNOPSIS +Writes a message to both the console and a log file. + +.PARAMETER Message +The message to be written. +#> +function Write-Log { + param( + [Parameter(Mandatory = $true)] + [string] $Message + ) + + [string] $logMessage = '{0:yyyy-MM-dd HH:mm:ss} {1}' -f (Get-Date), $Message + Write-Host $logMessage + Add-Content -Path $script:LogFile -Value $logMessage -Encoding UTF8 } -# Configure the scheduled tasks to run under the SYSTEM account. -# Note: In order to maintain compatibility with Windows Server 2008 R2, the ScheduledTasks module is not used. -schtasks.exe /Change /TN '\Microsoft\Windows\PowerShell\ScheduledJobs\DSInternals-RFM-Initializer' /RU SYSTEM | Out-Null -schtasks.exe /Change /TN '\Microsoft\Windows\PowerShell\ScheduledJobs\DSInternals-RFM-Resumer' /RU SYSTEM | Out-Null +#endregion Helper Functions -# Start the workflow task and let the magic happen. -Write-Host 'The LON-DC1 domain controller will now be restored from media. Up to 3 reboots will follow.' -pause -$initTask.RunAsTask() +# Execute the main function +Main ``` ## PARAMETERS diff --git a/Src/DSInternals.DataStore/DomainController.cs b/Src/DSInternals.DataStore/DomainController.cs index d1c900a..91e9d50 100644 --- a/Src/DSInternals.DataStore/DomainController.cs +++ b/Src/DSInternals.DataStore/DomainController.cs @@ -17,6 +17,7 @@ public class DomainController : IDisposable, IDomainController public const long EpochMinValue = 1; public const long EpochMaxValue = int.MaxValue; private const string CrossRefContainerRDN = "CN=Partitions"; + private const char DnsNameSeparator = '.'; // List of columns in the hiddentable: private const string ntdsSettingsCol = "dsa_col"; @@ -79,7 +80,7 @@ public DomainController(DirectoryContext context) // TODO: Export other database flags, not just IsADAM. // TODO: Load database health this.highestUSNCache = this.systemTableCursor.RetrieveColumnAsLong(highestCommitedUsnCol).Value; - + // Now we can load the Invocation ID and other information from the datatable: using (var dataTableCursor = context.OpenDataTable()) { @@ -104,12 +105,12 @@ public DomainController(DirectoryContext context) // Goto DC object (parent of NTDS): bool dcFound = dataTableCursor.GotoParentObject(schema); - + // Load data from the DC object - + // Load DC name: string dcName = dataTableCursor.RetrieveColumnAsString(schema.FindColumnId(CommonDirectoryAttributes.CommonName)); - + // DC name is null in the initial database, so use NTDS Settings object's CN instead this.Name = dcName ?? ntdsName; @@ -386,6 +387,14 @@ public string DNSHostName private set; } + public string HostName + { + get + { + return !String.IsNullOrEmpty(this.DNSHostName) ? this.DNSHostName.Split(DnsNameSeparator)[0] : this.Name; + } + } + public DistinguishedName ServerReference { get; diff --git a/Src/DSInternals.PowerShell/ADDBRestoreFromMediaScriptTemplate.ps1 b/Src/DSInternals.PowerShell/ADDBRestoreFromMediaScriptTemplate.ps1 index d201f93..b1977f5 100644 --- a/Src/DSInternals.PowerShell/ADDBRestoreFromMediaScriptTemplate.ps1 +++ b/Src/DSInternals.PowerShell/ADDBRestoreFromMediaScriptTemplate.ps1 @@ -1,161 +1,523 @@ <# .SYNOPSIS -Restores the {DCName} domain controller from its ntds.dit file. +Restores the {DCHostName} domain controller from its ntds.dit file. -.REMARKS +.DESCRIPTION + +This script performs a multi-phase domain controller restore from an IFM backup: +- Phase 0: Initiate the restore process and create a VSS backup. +- Phase 1: Set the local Administrator password if empty. +- Phase 2: Rename the computer and reboot if necessary. +- Phase 3: Install the required Windows features and reboot if needed. +- Phase 4: Promote the server to a domain controller. +- Phase 5: Restore the AD database, re-encrypt it, reset the DC password, and reconfigure LSA policies. +- Phase 6: Replace the SYSVOL directory, restore ACLs if available, and reboot the server. +- Phase 7: Reconfigure the SYSVOL replication subscription. + +Script exection logs can be found in the C:\Windows\Logs\DSInternals-RestoreFromMedia.txt file. + +.PARAMETER Phase +Specifies the phase of the restore operation to execute. Used to orchestrate the integrated recovery workflow. + +.NOTES This script should only be executed on a freshly installed {OSName}. Use at your own risk. The DSInternals PowerShell module must be installed for all users on the target server. +It is recommended to change the DSRM password after DC promotion. -Author: Michael Grafnetter +Author: Michael Grafnetter +Version: 2.0 #> -#Requires -Version 3 -Modules DSInternals,ServerManager,PSScheduledJob -RunAsAdministrator -# Perform a VSS backup before doing anything else. -Write-Host 'Creating a snapshot of the system drive to make rollback possible...' -$vssResult = ([wmiclass] 'Win32_ShadowCopy').Create("$env:SystemDrive\", 'ClientAccessible') +#Requires -Version 3 -Modules DSInternals,ServerManager -RunAsAdministrator + +param( + [Parameter(Mandatory = $false)] + [ValidateRange(0, 7)] + [int] $Phase = 0 +) -# The PS module must be present as workflows cannot contain non-existing activities. -Write-Host 'Installing the Active Directory module for Windows PowerShell...' -Add-WindowsFeature -Name RSAT-AD-PowerShell -ErrorAction Stop +# Make sure that the required data types and cmdlets are available. +Import-Module -Name DSInternals -ErrorAction Stop +Import-Module -Name ServerManager -ErrorAction Stop -# All the other operations will be executed by a restartable workflow running in the SYSTEM context. -Write-Host 'Registering restartable workflows...' +function Main { + [string] $script:LogFile = "$env:windir\Logs\DSInternals-RestoreFromMedia.txt" + Write-Log -Message "Starting script execution in phase $Phase..." -# Delete any pre-existing scheduled jobs with the same names before registering new ones. -Unregister-ScheduledJob -Name DSInternals-RFM-Initializer,DSInternals-RFM-Resumer -Force -ErrorAction SilentlyContinue + # The script must be executed locally so that it is accessible even after a reboot. + Test-ScriptPath -# The DSInternals-RFM-Initializer job will only be executed once in order to register the workflow and to invoke it for the first time. -$initTask = Register-ScheduledJob -Name DSInternals-RFM-Initializer -ScriptBlock { - workflow Restore-DomainController + switch($script:Phase) { - if ($env:COMPUTERNAME -ne '{DCName}') - { - # A server rename operation is required. - Rename-Computer -NewName '{DCName}' -Force - - # We explicitly suspend the workflow as Restart-Computer with the -Wait parameter does not survive local reboots. - shutdown.exe /r /t 5 - Suspend-Workflow -Label 'Waiting for reboot' + 0 { + Write-Log 'The {DCName} domain controller will now be restored from media. Up to 3 reboots will follow shortly.' + + # Perform a VSS backup before doing anything else. + New-VolumeShadowCopy -Volume $env:SystemDrive + + # Invoke the first phase in the background. + Register-ScheduledScript -ExecutePhase 1 + } + 1 { + # The local Administrator account must have a password set for dcpromo to succeed. + Reset-LocalAdministratorPassword -NewPassword '{DSRMPassword}' + + # Continue to the next phase. + Register-ScheduledScript -ExecutePhase 2 + } + 2 { + Write-Log -Message 'Checking the computer name...' + [bool] $computerRenameRequired = $env:COMPUTERNAME -ne '{DCName}' + + if ($computerRenameRequired) { + # A server rename operation is required. + # Note: The host name will automatically be truncated to 15 characters. + Write-Log -Message 'Renaming the computer to {DCHostName}...' + Rename-Computer -NewName '{DCHostName}' -Force -Verbose *>> $script:LogFile + } else { + Write-Log -Message 'The local system already has the correct name. Skipping the rename operation.' + } + + # Perform an optional reboot and continue to the next phase. + Register-ScheduledScript -ExecutePhase 3 -RebootRequired:$computerRenameRequired } + 3 { + Write-Log -Message 'Installing the required Windows features...' + + # Note: dcpromo.exe would install most of these features if absent. + # TODO: Check if these features are called the same in all OS versions. + [string[]] $featuresToInstall = @( + 'AD-Domain-Services', + 'DNS', + 'GPMC', + 'RSAT-ADDS', + 'RSAT-AD-AdminCenter', + 'RSAT-ADDS-Tools', + 'RSAT-AD-PowerShell', + 'RSAT-DNS-Server', + 'RSAT-DFS-Mgmt-Con', # dfsrdiag.exe is not installed by default + 'RSAT-Feature-Tools-BitLocker-BdeAducExt' # BitLocker Recovery Password Viewer is not installed by default + ) + + [object[]] $featuresRequiringRestart = Get-WindowsFeature | + Where-Object Name -in $featuresToInstall | + Install-WindowsFeature -IncludeManagementTools -IncludeAllSubFeature | + Where-Object RestartNeeded -ne ([Microsoft.Windows.ServerManager.Commands.RestartState]::No) 2>> $script:LogFile - if ((Get-Service NTDS -ErrorAction SilentlyContinue) -eq $null) - { - # A DC promotion is required. - # Note: In order to maintain compatibility with Windows Server 2008 R2, the ADDSDeployment module is not used. - # Advice: It is recommenced to change the DSRM password after DC promotion. - dcpromo.exe /unattend /ReplicaOrNewDomain:Domain /NewDomain:Forest /NewDomainDNSName:"{DomainName}" /DomainNetBiosName:"{NetBIOSDomainName}" /DomainLevel:{DomainMode} /ForestLevel:{ForestMode} '/SafeModeAdminPassword:"{DSRMPassword}"' /DatabasePath:"{TargetDBDirPath}" /LogPath:"{TargetLogDirPath}" /SysVolPath:"{TargetSysvolPath}" /AllowDomainReinstall:Yes /CreateDNSDelegation:No /DNSOnNetwork:No /InstallDNS:Yes /RebootOnCompletion:No - Set-ItemProperty -Path registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ServerManager\Roles\10 -Name ConfigurationStatus -Value 2 -Force - - <# Alternative approach for Winows Server 2012+ - Install-WindowsFeature -Name AD-Domain-Services - Install-ADDSForest -DomainName '{DomainName}' ` - -DomainNetbiosName '{NetBIOSDomainName}' ` - -ForestMode {ForestModeString} ` - -DomainMode {DomainModeString} ` - -DatabasePath "{TargetDBDirPath}" ` - -LogPath "{TargetLogDirPath}" ` - -SysvolPath "{TargetSysvolPath}" ` - -InstallDns ` - -CreateDnsDelegation:$false ` - -NoDnsOnNetwork ` - -SafeModeAdministratorPassword (ConvertTo-SecureString -String '{DSRMPassword}' -AsPlainText -Force)` - -NoRebootOnCompletion ` - -Force - #> + [bool] $restartNeeded = $null -ne $featuresRequiringRestart + + # Perform an optional reboot and continue to the next phase. + Register-ScheduledScript -ExecutePhase 4 -RebootRequired:$restartNeeded } + 4 { + # Check if the NTDS service is present and enabled, possibly by a previous script execution. + Write-Log -Message 'Checking the state of the NTDS service...' + [System.ServiceProcess.ServiceController] $ntdsService = Get-Service -Name NTDS -ErrorAction SilentlyContinue + + if ($null -eq $ntdsService -or $ntdsService.StartType -eq [System.ServiceProcess.ServiceStartMode]::Disabled) { + # A DC promotion is required. + Write-Log -Message 'Promoting the server to a domain controller...' + + # Note: In order to maintain compatibility with Windows Server 2008 R2, the ADDSDeployment PS module is not used. + dcpromo.exe /unattend /ReplicaOrNewDomain:Domain /NewDomain:Forest /NewDomainDNSName:"{DomainName}" /DomainNetBiosName:"{NetBIOSDomainName}" /DomainLevel:{DomainMode} /ForestLevel:{ForestMode} '/SafeModeAdminPassword:"{DSRMPassword}"' /DatabasePath:"{TargetDBDirPath}" /LogPath:"{TargetLogDirPath}" /SysVolPath:"{TargetSysvolPath}" /AllowDomainReinstall:Yes /CreateDNSDelegation:No /DNSOnNetwork:No /InstallDNS:Yes /RebootOnCompletion:No *>> $script:LogFile + } else { + Write-Log -Message 'The server is already a domain controller. Skipping dcpromo execution.' + } - # Reboot the computer into the Directory Services Restore Mode. - bcdedit.exe /set safeboot dsrepair - shutdown.exe /r /t 5 - Suspend-Workflow -Label 'Waiting for reboot' - - # Re-encrypt the DB with the new boot key. - $currentBootKey = Get-BootKey -Online - Set-ADDBBootKey -DatabasePath '{SourceDBPath}' -LogPath '{SourceLogDirPath}' -OldBootKey {OldBootKey} -NewBootKey $currentBootKey -Force - - # Clone the DC account password. - $ntdsParams = Get-ItemProperty -Path registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters - InlineScript { - # Note: SupplementalCredentials do not get serialized properly without using the InlineScript activity. - $dcAccount = Get-ADDBAccount -SamAccountName '{DCName}$' -DatabasePath $using:ntdsParams.'DSA Database file' -LogPath $using:ntdsParams.'Database log files path' -BootKey $using:currentBootKey - Set-ADDBAccountPasswordHash -ObjectGuid {DCGuid} -NTHash $dcAccount.NTHash -SupplementalCredentials $dcAccount.SupplementalCredentials -DatabasePath '{SourceDBPath}' -LogPath '{SourceLogDirPath}' -BootKey $using:currentBootKey -Force + # Prevent the Server Manager from saying that additional configuration is required. + # TODO: Test the following registry key on Windows Server 2008 R2. + Write-Log -Message 'Marking the AD DS role as already configured by the Server Manager...' + Set-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ServerManager\Roles\10' ` + -Name 'ConfigurationStatus' ` + -Value 2 ` + -Type DWord ` + -Force ` + -ErrorAction Continue ` + -Verbose *>> $script:LogFile + + # Avoid FSMO role holder being unavailable until it has completed replication of a writeable directory partition. + Write-Log -Message 'Disabling the initial datatabse synchronization...' + Set-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\NTDS\Parameters' ` + -Name 'Repl Perform Initial Synchronizations' ` + -Value 0 ` + -Type DWord ` + -Force ` + -Verbose *>> $script:LogFile + + # Continue with post-installation tasks. + Register-ScheduledScript -ExecutePhase 5 } + 5 { + # Make sure that AD DS is not running. + Write-Log -Message 'Checking the state of the NTDS service...' + [System.ServiceProcess.ServiceController] $ntdsService = Get-Service -Name NTDS -ErrorAction SilentlyContinue + + if($null -eq $ntdsService) { + Write-Log -Message 'Could not find the NTDS service. Terminating...' + break + } elseif ($ntdsService.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Running) { + Write-Log -Message 'Stopping the NTDS service...' + Stop-Service -Name NTDS -Force -Verbose *>> $script:LogFile + } else { + # Note: This is the most common case, as AD DS should not be running after a fresh dcpromo. + Write-Log -Message 'The NTDS service is stopped. Proceeding with database restoration...' + } + + # Re-encrypt the DB with the new boot key. + Write-Log -Message 'Fetching the current boot key (system key) from the HKLM\SYSTEM registry hive...' + [byte[]] $currentBootKey = Get-BootKey -Online 2>> $script:LogFile + + Write-Log -Message 'Re-encrypting the database with the new boot key...' + Set-ADDBBootKey -DatabasePath '{SourceDBPath}' ` + -LogPath '{SourceLogDirPath}' ` + -OldBootKey '{OldBootKey}' ` + -NewBootKey $currentBootKey ` + -ErrorAction Stop ` + -Force ` + -Verbose *>> $script:LogFile - # Replace the database and transaction logs. - robocopy.exe '{SourceDBDirPath}' $ntdsParams.'DSA Working Directory' *.dit *.edb *.chk *.jfm /MIR /NP /NDL /NJS - robocopy.exe '{SourceLogDirPath}' $ntdsParams.'Database log files path' *.log *.jrs /MIR /NP /NDL /NJS + # Fetch the DB and transaction log locations. + Write-Log -Message 'Fetching NTDS service parameters from the registry...' + [PSCustomObject] $ntdsParams = Get-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' 2>> $script:LogFile - # Replace SYSVOL. - robocopy.exe '{SourceSysvolPath}\{DomainName}' "{TargetSysvolPath}\domain" /MIR /XD DfsrPrivate /XJ /COPYALL /SECFIX /TIMFIX /NP /NDL + # Fetch the DC account password from the newly created DC. + Write-Log -Message 'Fetching the new DC account password hashes...' + [DSInternals.Common.Data.DSAccount] $dcAccount = Get-ADDBAccount -SamAccountName '{DCName}$' ` + -DatabasePath $ntdsParams.'DSA Database file' ` + -LogPath $ntdsParams.'Database log files path' ` + -BootKey $currentBootKey ` + -ErrorAction Stop 2>> $script:LogFile - # Reconfigure LSA policies. We would get into a BSOD loop if they do not match the corresponding values in the database. - Set-LsaPolicyInformation -DomainName '{NetBIOSDomainName}' -DnsDomainName '{DomainName}' -DnsForestName '{ForestName}' -DomainGuid {DomainGuid} -DomainSid {DomainSid} + # Clone the DC account password to the database that is being restored. + Write-Log -Message 'Cloning the DC account password hashes...' + Set-ADDBAccountPasswordHash -ObjectGuid '{DCGuid}' ` + -NTHash $dcAccount.NTHash ` + -SupplementalCredentials $dcAccount.SupplementalCredentials ` + -DatabasePath '{SourceDBPath}' ` + -LogPath '{SourceLogDirPath}' ` + -BootKey $currentBootKey ` + -Verbose ` + -Force *>> $script:LogFile - # Tell the DC that its DB has intentionally been restored. A new InvocationID will be generated as soon as the service starts. - Set-ItemProperty -Path registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters -Name 'Database restored from backup' -Value 1 -Force - Remove-ItemProperty -Path registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters -Name 'DSA Database Epoch' -Force + # Replace the database files using robocopy. + # Copy the database (*.dit, *.edb), checkpoint (*.chk), and flush map (*.jfm) files. + # /MIR: Mirrors the directory tree + # /NP: No progress + # /NDL: No directory list + # /NJS: No job summary + Write-Log -Message 'Replacing the AD database files...' + robocopy.exe '{SourceDBDirPath}' $ntdsParams.'DSA Working Directory' *.dit *.edb *.chk *.jfm /MIR /NP /NDL /NJS *>> $script:LogFile - # Disable DSRM and do one last reboot. - bcdedit.exe /deletevalue safeboot - shutdown.exe /r /t 5 - Suspend-Workflow -Label 'Waiting for reboot' + # Replace the transaction logs using robocopy. + # Copy the transaction logs (*.log) and reserved transaction log files (*.jrs). + # /MIR: Mirrors the directory tree + # /NP: No progress + # /NDL: No directory list + # /NJS: No job summary + Write-Log -Message 'Replacing the AD database transaction log files...' + robocopy.exe '{SourceLogDirPath}' $ntdsParams.'Database log files path' *.log *.jrs /MIR /NP /NDL /NJS *>> $script:LogFile - # Reconfigure SYSVOL replication in case it has been restored to a different path. + # Reconfigure LSA policies. We would get into a BSOD loop if they do not match the corresponding values in the database. + Write-Log -Message 'Reconfiguring the LSA policies...' + Set-LsaPolicyInformation -DomainName '{NetBIOSDomainName}' ` + -DnsDomainName '{DomainName}' ` + -DnsForestName '{ForestName}' ` + -DomainGuid '{DomainGuid}' ` + -DomainSid '{DomainSid}' ` + -Verbose *>> $script:LogFile - # Update DFS-R subscription if present in AD. - $dfsrSubscriptionDN = 'CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,{DCDistinguishedName}' - $dfsrSubscription = Set-ADObject -Identity $dfsrSubscriptionDN -Server localhost -PassThru -ErrorAction SilentlyContinue -Replace @{ - 'msDFSR-RootPath' = "{TargetSysvolPath}\domain"; - 'msDFSR-StagingPath' = "{TargetSysvolPath}\staging areas\{DomainName}" + # Tell the DC that its DB has intentionally been restored. A new InvocationID will be generated as soon as the service starts. + Write-Log -Message 'Marking the database as restored from backup...' + Set-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' ` + -Name 'Database restored from backup' ` + -Value 1 ` + -Type DWord ` + -Force ` + -Verbose *>> $script:LogFile + + # Remove the DSA Database Epoch value to bypass database rollback detection. + Write-Log -Message 'Clearing the DSA Database Epoch value...' + Remove-ItemProperty -Path 'registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' ` + -Name 'DSA Database Epoch' ` + -Force ` + -Verbose *>> $script:LogFile + + # Reboot and continue to the next phase. + Register-ScheduledScript -ExecutePhase 6 -RebootRequired } + 6 { + # Replace the SYSVOL directory. + # /MIR: Mirrors the directory tree + # /XD: Excludes the DfsrPrivate directory from being copied. + # /XJ: Excludes junction points, several of which are present in SYSVOL. + # /COPYALL: Copies all file information, including data, attributes, timestamps, NTFS ACLs (permissions), owner information, and auditing information. + # /SECFIX: Fixes file security on all files, even skipped ones. + # /TIMFIX: This fixes file times on all files, even skipped ones. + # /NP: No progress + # /NDL: No directory list + Write-Log -Message 'Replacing the SYSVOL files...' + [string] $sourcePath = Join-Path -Path '{SourceSysvolPath}' -ChildPath '{DomainName}' + [string] $targetPath = Join-Path -Path "{TargetSysvolPath}" -ChildPath 'domain' + robocopy.exe $sourcePath $targetPath /MIR /XD DfsrPrivate /XJ /COPYALL /SECFIX /TIMFIX /NP /NDL *>> $script:LogFile + + # Check if an optional SYSVOL Group Policy ACL backup is present. + # This would be useful in situations where the SYSVOL is backed up to a non-NTFS file system. + [string] $sysvolAclBackupPath = Join-Path -Path '{SourceSysvolPath}' -ChildPath 'PolicyPermissions.txt' + if(Test-Path -Path $sysvolAclBackupPath -PathType Leaf) { + Write-Log -Message 'Restoring the SYSVOL Group Policy ACLs...' + [string] $aclRestorePath = Join-Path -Path "{TargetSysvolPath}" -ChildPath 'domain\Policies' + icacls.exe $aclRestorePath /restore $sysvolAclBackupPath *>> $script:LogFile + } else { + Write-Log -Message 'No SYSVOL ACL backup found. Skipping the ACL restoration.' + } - if($dfsrSubscription -ne $null) - { - # Download the updated DFS-R configuration from AD. - Invoke-WmiMethod -Class DfsrConfig -Name PollDsNow -ArgumentList localhost -Namespace ROOT\MicrosoftDfs + # A reboot is required for AD to start normally. + Register-ScheduledScript -ExecutePhase 7 -RebootRequired } + 7 { + # Reconfigure SYSVOL replication in case it has been restored to a different path. + + # Make sure that AD Web Services are available. + [System.ServiceProcess.ServiceController] $adws = Get-Service -Name NTDS -ErrorAction SilentlyContinue + + if($null -eq $adws -or $adws.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Running) { + Write-Log -Message 'AD Web Services are not available. Terminating...' + break + } else { + Write-Log -Message 'The AD Web Services service is running. Proceeding with SYSVOL subscription reconfiguration...' + } + + # Update DFS-R subscription if present in AD. + Update-DfsrSubscription -DomainControllerDN '{DCDistinguishedName}' ` + -SysvolPath "{TargetSysvolPath}" ` + -DomainName '{DomainName}' - # Update FRS subscription if present in AD. - $frsSubscriptionDN = 'CN=Domain System Volume (SYSVOL share),CN=NTFRS Subscriptions,{DCDistinguishedName}' - $frsSubscription = Set-ADObject -Identity $frsSubscriptionDN -Server localhost -PassThru -ErrorAction SilentlyContinue -Replace @{ - 'fRSRootPath' = "{TargetSysvolPath}\domain"; - 'fRSStagingPath' = "{TargetSysvolPath}\staging\domain" + # Update FRS subscription if present in AD. + Update-FrsSubscription -DomainControllerDN '{DCDistinguishedName}' ` + -SysvolPath "{TargetSysvolPath}" } + } + + if($Phase -ge 1) { + [string] $taskName = "DSInternals-RFM-Phase$Phase" + Write-Log -Message "Removing the scheduled task $taskName..." + schtasks.exe /Delete /TN $taskName /F *>> $script:LogFile + } + + Write-Log -Message "Execution of phase $Phase has finished." +} + +#region Helper Functions + +<# +.SYNOPSIS +Resets the password of the local Administrator account (RID=500) +if it has not been set yet. + +.NOTES +This recovery script intentionally contains plaintext passwords. +Using a SecureString in this function would not provide any additional security. +The corresponding PSScriptAnalyzer warning is therefore suppressed. + +#> +function Reset-LocalAdministratorPassword { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] + param( + [Parameter(Mandatory = $true)] + [string] $NewPassword + ) + + # Note: Due to compatibility reasons, the Get-LocalUser cmdlet is not used. + Write-Log -Message 'Fetching built-in Administrator account information through WMI...' + [wmi] $builtinAdminWMI = Get-WmiObject -Class Win32_UserAccount -Filter 'SID LIKE "%-500"' -Property Name + + Write-Log -Message 'Fetching built-in Administrator account information through ADSI...' + [adsi] $builtinAdminADSI = 'WinNT://./{0},User' -f $builtinAdminWMI.Name + [bool] $hasPassword = $builtinAdminADSI.PasswordAge.Value -gt 0 + + if(-not $hasPassword) { + Write-Log 'Setting a password for the local Administrator account...' + $builtinAdminWMI.SetPassword($NewPassword) *>> $script:LogFile + } else { + Write-Log 'A password for the local Administrator account has already been set. No action is required.' + } +} - if($frsSubscription -ne $null) - { - # Download the updated FRS configuration from AD. - InlineScript { ntfrsutl.exe poll /now } +<# +.SYNOPSIS +Creates a new volume shadow copy of the specified volume. +#> +function New-VolumeShadowCopy { + param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^[A-Z]:$')] + [string] $Volume + ) + + Write-Log 'Creating a snapshot of the system drive to make rollback possible...' + ([wmiclass] 'Win32_ShadowCopy').Create("$Volume\", 'ClientAccessible') *>> $script:LogFile +} + +function Register-ScheduledScript { + param( + [Parameter(Mandatory = $true)] + [int] $ExecutePhase, + + [Parameter(Mandatory = $false)] + [switch] $RebootRequired + ) + + # Each phase of the script is executed by a separate scheduled task. + [string] $taskName = "DSInternals-RFM-Phase$ExecutePhase" + + # Locate powershell.exe + [string] $psPath = Get-Command -Name 'powershell.exe' -CommandType Application | Select-Object -ExpandProperty Path + + # Locate the current PS script + [string] $scriptPath = $script:MyInvocation.MyCommand.Source + + # Generate the complete command line + [string] $commandLine = '"{0}" -ExecutionPolicy Bypass -NonInteractive -NoProfile -NoLogo -File "{1}" -Phase {2}' -f $psPath,$scriptPath,$ExecutePhase + + # NOTE: The ScheduledTasks module is not used because it is not available on Windows Server 2008 R2. + Write-Log -Message 'Registering the script to be executed as a scheduled task...' + schtasks.exe /Create /TN $taskName /TR $commandLine /SC ONSTART /RU SYSTEM /RL HIGHEST /F *>> $script:LogFile + + if($RebootRequired) { + # Reboot the computer and let it automatically execute the scheduled task. + Write-Log -Message 'Rebooting the computer to continue the restore process...' + shutdown.exe /r /t 5 /f *>> $script:LogFile + } else { + # Start the scheduled task immediately. + Write-Log -Message 'Starting the scheduled task...' + schtasks.exe /Run /I /TN $taskName *>> $script:LogFile + } +} + +<# +.SYNOPSIS +Checks whether the script is being executed from a local file. +#> +function Test-ScriptPath { + Write-Log -Message 'Checking if the script is being executed from a local file...' + + # Check if the PSScriptRoot variable is set. + [bool] $isScript = -not [string]::IsNullOrEmpty($PSScriptRoot) + + if(-not $isScript) { + Write-Log -Message 'Not running as script. Terminating...' + throw 'The script must be executed from a local file.' + } else { + if(-not ([uri] $PSScriptRoot).IsUnc) { + Write-Log -Message 'The script is being executed from a local file.' + } else { + Write-Log -Message 'Running from a UNC path. Terminating...' + throw 'The script must be executed from a local file.' } } +} + +<# +.SYNOPSIS +Updates the DFS-R subscription if present in AD. - # Delete any pre-existing workflows with the same name before starting a new one. - Remove-Job -Name DSInternals-RFM-Workflow -Force -ErrorAction SilentlyContinue +.PARAMETER DomainControllerDN +The distinguished name of the domain controller computer account. - # Start the workflow. - Restore-DomainController -JobName DSInternals-RFM-Workflow +.PARAMETER SysvolPath +The path to the SYSVOL share on the target domain controller. +#> +function Update-DfsrSubscription { + param( + [Parameter(Mandatory = $true)] + [string] $DomainControllerDN, + + [Parameter(Mandatory = $true)] + [string] $SysvolPath, + + [Parameter(Mandatory = $true)] + [string] $DomainName + ) + + # Make sure that the required data types and cmdlets are available. + Import-Module -Name ActiveDirectory -ErrorAction Stop + + Write-Log -Message 'Updating the FRS subscription object in AD...' + [string] $dfsrSubscriptionDN = "CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,$DomainControllerDN" + [Microsoft.ActiveDirectory.Management.ADObject] $dfsrSubscription = Set-ADObject -Identity $dfsrSubscriptionDN -Server localhost -PassThru -ErrorAction SilentlyContinue -Replace @{ + 'msDFSR-RootPath' = Join-Path -Path $SysvolPath -ChildPath 'domain' + 'msDFSR-StagingPath' = Join-Path -Path $SysvolPath -ChildPath "staging areas\$DomainName" + } + + if($null -ne $dfsrSubscription) { + # Download the updated DFS-R configuration from AD. + Write-Log -Message 'Polling AD for DFS-R configuration changes...' + Invoke-WmiMethod -Class DfsrConfig -Name PollDsNow -ArgumentList localhost -Namespace ROOT\MicrosoftDfs *>> $script:LogFile + } else { + Write-Log -Message 'DFS-R subscription was not found in AD. Has the domain not yet been migrated from FRS?' + } } -# The DSInternals-RFM-Resumer job will be executed after each reboot to unpause the workflow. -$resumeTask = Register-ScheduledJob -Name DSInternals-RFM-Resumer -Trigger (New-JobTrigger -AtStartup) -ScriptBlock { - # Unregister the one-time task that must already have been executed. - Unregister-ScheduledJob -Name DSInternals-RFM-Initializer -Force -ErrorAction SilentlyContinue +<# +.SYNOPSIS +Updates the FRS subscription if present in AD. + +.PARAMETER DomainControllerDN +The distinguished name of the domain controller computer account. + +.PARAMETER SysvolPath +The path to the SYSVOL share on the target domain controller. +#> +function Update-FrsSubscription { + param( + [Parameter(Mandatory = $true)] + [string] $DomainControllerDN, + + [Parameter(Mandatory = $true)] + [string] $SysvolPath + ) + + # Make sure that the required data types and cmdlets are available. + Import-Module -Name ActiveDirectory -ErrorAction Stop - # Resume the workflow after the computer is rebooted. - Resume-Job -Name DSInternals-RFM-Workflow -Wait | Wait-Job | where State -In Completed,Failed,Stopped | foreach { - # Perform cleanup when finished. - Remove-Job -Job $PSItem -Force - Unregister-ScheduledJob -Name DSInternals-RFM-Resumer -Force + # Update FRS subscription if present in AD. + Write-Log -Message 'Updating the FRS subscription object in AD...' + [string] $frsSubscriptionDN = "CN=Domain System Volume (SYSVOL share),CN=NTFRS Subscriptions,$DomainControllerDN" + [Microsoft.ActiveDirectory.Management.ADObject] $frsSubscription = Set-ADObject -Identity $frsSubscriptionDN -Server localhost -PassThru -Verbose -ErrorAction SilentlyContinue -Replace @{ + 'fRSRootPath' = Join-Path -Path $SysvolPath -ChildPath 'domain' + 'fRSStagingPath' = Join-Path -Path $SysvolPath -ChildPath 'staging\domain' } + + if($null -ne $frsSubscription) { + # Download the updated FRS configuration from AD. + Write-Log -Message 'Polling AD for FRS configuration changes...' + ntfrsutl.exe poll /now *>> $script:LogFile + # TODO: Check what happens if the FRS service is disabled on the new DC. + } else { + Write-Log -Message 'FRS subscription was not found in AD. This is expected.' + } +} + +<# +.SYNOPSIS +Writes a message to both the console and a log file. + +.PARAMETER Message +The message to be written. +#> +function Write-Log { + param( + [Parameter(Mandatory = $true)] + [string] $Message + ) + + [string] $logMessage = '{0:yyyy-MM-dd HH:mm:ss} {1}' -f (Get-Date), $Message + Write-Host $logMessage + Add-Content -Path $script:LogFile -Value $logMessage -Encoding UTF8 } -# Configure the scheduled tasks to run under the SYSTEM account. -# Note: In order to maintain compatibility with Windows Server 2008 R2, the ScheduledTasks module is not used. -schtasks.exe /Change /TN '\Microsoft\Windows\PowerShell\ScheduledJobs\DSInternals-RFM-Initializer' /RU SYSTEM | Out-Null -schtasks.exe /Change /TN '\Microsoft\Windows\PowerShell\ScheduledJobs\DSInternals-RFM-Resumer' /RU SYSTEM | Out-Null +#endregion Helper Functions -# Start the workflow task and let the magic happen. -Write-Host 'The {DCName} domain controller will now be restored from media. Up to 3 reboots will follow.' -pause -$initTask.RunAsTask() +# Execute the main function +Main diff --git a/Src/DSInternals.PowerShell/Commands/Datastore/NewADDBRestoreFromMediaScriptCommand.cs b/Src/DSInternals.PowerShell/Commands/Datastore/NewADDBRestoreFromMediaScriptCommand.cs index 3fa9637..1ad7bc3 100644 --- a/Src/DSInternals.PowerShell/Commands/Datastore/NewADDBRestoreFromMediaScriptCommand.cs +++ b/Src/DSInternals.PowerShell/Commands/Datastore/NewADDBRestoreFromMediaScriptCommand.cs @@ -48,7 +48,7 @@ public SecureString SafeModeAdministratorPassword { get; set; - } + } protected override void ProcessRecord() { @@ -86,6 +86,7 @@ protected override void ProcessRecord() string template = LoadScriptTemplate(); var script = new StringBuilder(template). Replace("{DCName}", dc.Name). + Replace("{DCHostName}", dc.HostName). Replace("{DCGuid}", dc.Guid.ToString()). Replace("{DCDistinguishedName}", dc.ServerReference.ToString()). Replace("{DomainName}", dc.DomainName).