Public/New-Tier0AdminAccount.ps1
function New-Tier0AdminAccount { <# .SYNOPSIS Creates and secures Tier0 administrative accounts. .DESCRIPTION This function creates or updates a new Tier0 administrative account, making it member of administrative groups, securing it properly, and configuring security attributes according to best practices. It also secures the built-in Administrator account. .PARAMETER ConfigXMLFile [System.IO.FileInfo] Full path to the XML configuration file. Contains all naming conventions, OU structure, and security settings. Must be a valid XML file with required schema elements. Default: C:\PsScripts\Config.xml .PARAMETER DMScripts [System.String] Path to all the scripts and files needed by this function. Must contain a SecTmpl subfolder and may contain a Pic subfolder for user pictures. Default: C:\PsScripts\ .EXAMPLE New-Tier0AdminAccount -ConfigXMLFile C:\PsScripts\Config.xml Creates or updates Tier0 admin accounts using the specified configuration file. .EXAMPLE New-Tier0AdminAccount -ConfigXMLFile C:\PsScripts\Config.xml -DMScripts C:\Scripts Creates or updates Tier0 admin accounts using the specified configuration file and scripts path. .INPUTS [System.IO.FileInfo] [System.String] .OUTPUTS [System.Void] .NOTES Used Functions: Name ║ Module/Namespace ═══════════════════════════════════════════╬══════════════════════════════ Import-MyModule ║ EguibarIT Get-FunctionDisplay ║ EguibarIT Get-SafeVariable ║ EguibarIT Add-AdGroupNesting ║ EguibarIT Remove-Everyone ║ EguibarIT.DelegationPS Remove-PreWin2000 ║ EguibarIT.DelegationPS Get-ADUser ║ ActiveDirectory Get-ADGroup ║ ActiveDirectory Set-ADUser ║ ActiveDirectory New-ADUser ║ ActiveDirectory Move-ADObject ║ ActiveDirectory Set-ADObject ║ ActiveDirectory .NOTES Version: 1.1 DateModified: 7/May/2025 LastModifiedBy: Vicente Rodriguez Eguibar vicente@eguibar.com Eguibar IT http://www.eguibarit.com .LINK https://github.com/vreguibar/EguibarIT .COMPONENT Active Directory .ROLE Administrator .FUNCTIONALITY User Management, Security Hardening #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'High' )] [OutputType([System.Void])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ValueFromRemainingArguments = $false, HelpMessage = 'Full path to the configuration.xml file', Position = 0)] [ValidateScript({ if (-Not ($_ | Test-Path -PathType Leaf) ) { throw ('File not found: {0}' -f $_) } if ($_.Extension -ne '.xml') { throw ('File must be XML: {0}' -f $_) } try { [xml]$xml = Get-Content -Path $_ -ErrorAction Stop # Verify required XML elements are present if ($null -eq $xml.n.Admin -or $null -eq $xml.n.Admin.Users -or $null -eq $xml.n.Admin.OUs -or $null -eq $xml.n.RegisteredOrg -or $null -eq $xml.n.DefaultPassword -or $null -eq $xml.n.NC) { throw 'XML file is missing required elements (Admin, Users, OUs, RegisteredOwner, DefaultPassword or NC section)' } return $true } catch { throw ('Invalid XML file: {0}' -f $_.Exception.Message) } })] [PSDefaultValue(Help = 'Default Value is "C:\PsScripts\Config.xml"', Value = 'C:\PsScripts\Config.xml' )] [Alias('Config', 'XML', 'ConfigXml')] [System.IO.FileInfo] $ConfigXMLFile, [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ValueFromRemainingArguments = $false, HelpMessage = 'Path to all the scripts and files needed by this function', Position = 1)] [PSDefaultValue( Help = 'Default Value is "C:\PsScripts\"', Value = 'C:\PsScripts\' )] [Alias('ScriptPath', 'DelegationModelScripts', 'DMScriptsPath')] [string] $DMScripts = 'C:\PsScripts\', [Parameter(Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false, ValueFromRemainingArguments = $false, HelpMessage = 'Start transcript logging to DMScripts path with function name', Position = 2)] [Alias('Transcript', 'Log')] [switch] $EnableTranscript ) Begin { Set-StrictMode -Version Latest If (-not $PSBoundParameters.ContainsKey('ConfigXMLFile')) { $PSBoundParameters['ConfigXMLFile'] = 'C:\PsScripts\Config.xml' } #end If If (-not $PSBoundParameters.ContainsKey('DMScripts')) { $PSBoundParameters['DMScripts'] = 'C:\PsScripts\' } #end If # If EnableTranscript is specified, start a transcript if ($EnableTranscript) { # Ensure DMScripts directory exists if (-not (Test-Path -Path $DMScripts -PathType Container)) { try { New-Item -Path $DMScripts -ItemType Directory -Force | Out-Null Write-Verbose -Message ('Created transcript directory: {0}' -f $DMScripts) } catch { Write-Warning -Message ('Failed to create transcript directory: {0}' -f $_.Exception.Message) } #end try-catch } #end if # Create transcript filename using function name and current date/time $TranscriptFile = Join-Path -Path $DMScripts -ChildPath ('{0}_{1}.LOG' -f $MyInvocation.MyCommand.Name, (Get-Date -Format 'yyyyMMdd_HHmmss')) try { Start-Transcript -Path $TranscriptFile -Force -ErrorAction Stop Write-Verbose -Message ('Transcript started: {0}' -f $TranscriptFile) } catch { Write-Warning -Message ('Failed to start transcript: {0}' -f $_.Exception.Message) } #end try-catch } #end if # Display function header if variables exist if ($null -ne $Variables -and $null -ne $Variables.Header) { $txt = ($Variables.Header -f (Get-Date).ToString('dd/MMM/yyyy'), $MyInvocation.Mycommand, (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False) ) Write-Verbose -Message $txt } #end If ############################## # Module imports Import-MyModule -Name 'ActiveDirectory' -Verbose:$false Import-MyModule -Name 'EguibarIT' -Verbose:$false Import-MyModule -Name 'EguibarIT.DelegationPS' -Verbose:$false ############################## # Variables Definition # parameters variable for splatting CMDlets [hashtable]$Splat = [hashtable]::New([StringComparer]::OrdinalIgnoreCase) # Define current domain controller [string]$CurrentDC = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().FindDomainController().Name # Load the XML configuration file try { # Use ConfigXMLFile directly from parameter rather than from PSBoundParameters [xml]$ConfXML = [xml](Get-Content $ConfigXMLFile) } catch { Write-Error -Message ('Error reading XML file: {0}' -f $_.Exception.Message) throw } #end Try-Catch # Set admin names [string]$NewAdminName = $ConfXML.n.Admin.users.NEWAdmin.Name #region Users $AdminName = Get-SafeVariable -Name 'AdminName' -CreateIfNotExist { try { Get-ADUser -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-500' } } catch { Write-Debug -Message ('Failed to retrieve Administrator account: {0}' -f $_.Exception.Message) $null } } #endregion Users #region Well-Known groups Variables $DomainAdmins = Get-SafeVariable -Name 'DomainAdmins' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-512' } } catch { Write-Debug -Message ('Failed to retrieve Domain Admins group: {0}' -f $_.Exception.Message) $null } } $EnterpriseAdmins = Get-SafeVariable -Name 'EnterpriseAdmins' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-519' } } catch { Write-Debug -Message ('Failed to retrieve Enterprise Admins group: {0}' -f $_.Exception.Message) $null } } $GPOCreatorsOwner = Get-SafeVariable -Name 'GPOCreatorsOwner' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-520' } } catch { Write-Debug -Message ('Failed to retrieve GPO Creators Owner group: {0}' -f $_.Exception.Message) $null } } $DeniedRODC = Get-SafeVariable -Name 'DeniedRODC' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-572' } } catch { Write-Debug -Message ('Failed to retrieve Denied RODC Password Replication group: {0}' -f $_.Exception.Message) $null } } #endregion Well-Known groups Variables # Generate DN paths for OUs [string]$ItAdminOu = $ConfXML.n.Admin.OUs.ItAdminOU.name [string]$ItAdminAccountsOu = $ConfXML.n.Admin.OUs.ItAdminAccountsOU.name [string]$ItAdminAccountsOuDn = ('OU={0},OU={1},{2}' -f $ItAdminAccountsOu, $ItAdminOu, $Variables.AdDn) } #end Begin Process { if ($PSCmdlet.ShouldProcess('Active Directory Identity', 'Create and Secure Tier0 Admin Accounts')) { # Try to get the new Admin $NewAdminExists = Get-ADUser -Filter { SamAccountName -eq $NewAdminName } -ErrorAction SilentlyContinue # Get picture if exist. Use default if not. if (Test-Path -Path ('{0}Pic\{1}.jpg' -f $PSBoundParameters['DMScripts'], $NewAdminName)) { # Read the path and file name of JPG picture $PhotoFile = '{0}Pic\{1}.jpg' -f $PSBoundParameters['DMScripts'], $NewAdminName # Get the content of the JPG file [byte[]]$Photo = [System.IO.File]::ReadAllBytes($PhotoFile) } else { if (Test-Path -Path ('{0}Pic\Default.jpg' -f $PSBoundParameters['DMScripts'])) { # Read the path and file name of JPG picture $PhotoFile = '{0}Pic\Default.jpg' -f $PSBoundParameters['DMScripts'] # Get the content of the JPG file [byte[]]$Photo = [System.IO.File]::ReadAllBytes($PhotoFile) } else { $Photo = $null } #end If-Else } #end If-Else # Check if the new Admin account already exist. If not, then create it. if ($NewAdminExists) { # The user was found. Proceed to modify it accordingly. $Splat = @{ Enabled = $true UserPrincipalName = ('{0}@{1}' -f $NewAdminName, $env:USERDNSDOMAIN) SamAccountName = $NewAdminName DisplayName = $NewAdminName Description = $ConfXML.n.Admin.users.NEWAdmin.description employeeId = '0123456' TrustedForDelegation = $false AccountNotDelegated = $true Company = $ConfXML.n.RegisteredOrg Country = 'MX' Department = $ConfXML.n.Admin.users.NEWAdmin.department State = 'Puebla' EmailAddress = ('{0}@{1}' -f $NewAdminName, $env:USERDNSDOMAIN) Replace = @{ 'employeeType' = $ConfXML.n.NC.AdminAccSufix0 'msNpAllowDialin' = $false 'msDS-SupportedEncryptionTypes' = '24' } } # If photo exist, add it to parameters if ($Photo) { # Only if photo exists, add it to splatting $Splat.Replace.Add('thumbnailPhoto', $Photo) } #end If # Update the existing admin user Set-ADUser -Identity $NewAdminName @Splat } else { # User was not Found! create new. $Splat = @{ Path = $ItAdminAccountsOuDn Name = $NewAdminName AccountPassword = (ConvertTo-SecureString -String $ConfXML.n.DefaultPassword -AsPlainText -Force) ChangePasswordAtLogon = $false Enabled = $true UserPrincipalName = ('{0}@{1}' -f $NewAdminName, $env:USERDNSDOMAIN) SamAccountName = $NewAdminName DisplayName = $NewAdminName Description = $ConfXML.n.Admin.users.NEWAdmin.description employeeId = '0123456' TrustedForDelegation = $false AccountNotDelegated = $true Company = $ConfXML.n.RegisteredOrg Country = $ConfXML.n.Admin.users.NEWAdmin.Country Department = $ConfXML.n.Admin.users.NEWAdmin.department State = $ConfXML.n.Admin.users.NEWAdmin.State EmailAddress = ('{0}@{1}' -f $NewAdminName, $env:USERDNSDOMAIN) OtherAttributes = @{ 'employeeType' = $ConfXML.n.NC.AdminAccSufix0 'msNpAllowDialin' = $false 'msDS-SupportedEncryptionTypes' = '24' } } if ($Photo) { # Only if photo exists, add it to splatting $Splat.OtherAttributes.Add('thumbnailPhoto', $Photo) } #end If # Create the new Admin with special values try { New-ADUser @Splat } catch { Write-Error -Message ('Error when creating new Admin account: {0}' -f $_.Exception.Message) throw } #end Try-Catch # Note on encryption types: # msDS-SupportedEncryptionTypes: # Kerberos DES Encryption = 2 # Kerberos AES 128 = 8 # Kerberos AES 256 = 16 # Value 24 = AES 128 + AES 256 } #end If-Else new user created # Move AD object to proper OU Get-ADUser -Identity $NewAdminName | Move-ADObject -TargetPath $ItAdminAccountsOuDn -Server $CurrentDC # Refresh objects variables $AdminName = Get-ADUser -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-500' } $NewAdminExists = Get-ADUser -Identity $NewAdminName # Set the Protect against accidental deletions attribute # Identity ONLY accepts DistinguishedName or GUID -- DN fails I don't know why Set-ADObject -Identity $AdminName.ObjectGUID -ProtectedFromAccidentalDeletion $true Set-ADObject -Identity $NewAdminExists.ObjectGUID -ProtectedFromAccidentalDeletion $true # Make it member of administrative groups Add-AdGroupNesting -Identity $DomainAdmins -Members $NewAdminExists Add-AdGroupNesting -Identity $EnterpriseAdmins -Members $NewAdminExists Add-AdGroupNesting -Identity $GPOCreatorsOwner -Members $NewAdminExists Add-AdGroupNesting -Identity $DeniedRODC -Members $NewAdminExists # Security hardening # http://blogs.msdn.com/b/muaddib/archive/2013/12/30/how-to-modify-security-inheritance-on-active-directory-objects.aspx #### # Remove Everyone group from Admin-User & Administrator Remove-Everyone -LDAPpath $NewAdminExists.DistinguishedName Remove-Everyone -LDAPpath $AdminName.DistinguishedName #### # Remove AUTHENTICATED USERS group from Admin-User & Administrator # Uncomment if needed #Remove-AuthUser -LDAPPath $NewAdminExists.DistinguishedName #Remove-AuthUser -LDAPPath ('CN={0},{1}' -f $AdminName, $ItAdminAccountsOuDn) #### # Remove Pre-Windows 2000 Compatible Access group from Admin-User & Administrator Remove-PreWin2000 -LDAPpath $NewAdminExists.DistinguishedName Remove-PreWin2000 -LDAPpath $AdminName.DistinguishedName # Configure the built-in Administrator account $Params = @{ 'employeeType' = $ConfXML.n.NC.AdminAccSufix0 'msNpAllowDialin' = $false 'msDS-SupportedEncryptionTypes' = 24 } # Get picture for built-in Administrator if exists if (Test-Path -Path ('{0}Pic\{1}.jpg' -f $PSBoundParameters['DMScripts'], $AdminName.SamAccountName)) { # Read the path and file name of JPG picture $PhotoFile = '{0}Pic\{1}.jpg' -f $PSBoundParameters['DMScripts'], $AdminName.SamAccountName # Get the content of the JPG file [byte[]]$Photo = [System.IO.File]::ReadAllBytes($PhotoFile) } #end If if ($Photo) { # Only if photo exists, add it to splatting $Params.Add('thumbnailPhoto', $Photo) } #end If # Apply settings to the built-in Administrator account $Splat = @{ Identity = $AdminName TrustedForDelegation = $false AccountNotDelegated = $true Replace = $Params Server = $CurrentDC } Set-ADUser @Splat } #end If ShouldProcess } #end Process End { # Display function footer if variables exist if ($null -ne $Variables -and $null -ne $Variables.Footer) { $txt = ($Variables.Footer -f $MyInvocation.InvocationName, 'Create and Secure Tier0 Admin Accounts.' ) Write-Verbose -Message $txt } #end If # Stop transcript if it was started if ($EnableTranscript) { try { Stop-Transcript -ErrorAction Stop Write-Verbose -Message 'Transcript stopped successfully' } catch { Write-Warning -Message ('Failed to stop transcript: {0}' -f $_.Exception.Message) } #end Try-Catch } #end If } #end End } #end Function New-Tier0AdminAccount |