Public/New-Tier0AdminGroup.ps1
function New-Tier0AdminGroup { <# .SYNOPSIS Creates Tier0 administrative groups in Active Directory following the tiered administration model. .DESCRIPTION This function creates all the necessary security groups needed for implementing a tiered administrative model. It creates both domain local and global security groups as defined in the configuration XML file. The groups are created in the appropriate OUs and are protected from accidental deletion. This follows Microsoft's recommended tiered administration model with separation of privilege. .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 for security templates. Default: C:\PsScripts\ .EXAMPLE New-Tier0AdminGroup -ConfigXMLFile C:\PsScripts\Config.xml Creates all Tier0 admin groups using the specified configuration file. .EXAMPLE New-Tier0AdminGroup -ConfigXMLFile C:\PsScripts\Config.xml -DMScripts C:\Scripts Creates Tier0 admin groups 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 New-AdDelegatedGroup ║ EguibarIT Set-ADObject ║ ActiveDirectory Move-ADObject ║ ActiveDirectory Get-ADGroup ║ ActiveDirectory .NOTES Version: 1.0 DateModified: 29/Apr/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 Group 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.OUs -or $null -eq $xml.n.Admin.LG -or $null -eq $xml.n.Admin.GG -or $null -eq $xml.n.Servers.GG -or $null -eq $xml.n.Servers.LG -or $null -eq $xml.n.NC) { throw 'XML file is missing required elements (Admin, OUs, LG, GG, Servers.GG, Servers.LG 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')] [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 ($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) # Collection to store all groups for later processing [System.Collections.Generic.HashSet[object]]$AllGroups = [System.Collections.Generic.HashSet[object]]::New() # Load the XML configuration file try { [xml]$ConfXML = [xml](Get-Content $PSBoundParameters['ConfigXMLFile']) } catch { Write-Error -Message ('Error reading XML file: {0}' -f $_.Exception.Message) throw } #end Try-Catch # Load naming conventions from XML [hashtable]$NC = @{ 'sl' = $confXML.n.NC.LocalDomainGroupPreffix 'sg' = $confXML.n.NC.GlobalGroupPreffix 'su' = $confXML.n.NC.UniversalGroupPreffix 'Delim' = $confXML.n.NC.Delimiter 'T0' = $confXML.n.NC.AdminAccSufix0 'T1' = $confXML.n.NC.AdminAccSufix1 'T2' = $confXML.n.NC.AdminAccSufix2 } # Generate DN paths for OUs [string]$ItAdminOu = $ConfXML.n.Admin.OUs.ItAdminOU.name [string]$ItAdminGroupsOu = $ConfXML.n.Admin.OUs.ItAdminGroupsOU.name [string]$ItRightsOu = $ConfXML.n.Admin.OUs.ItRightsOU.name [string]$ItPrivGroupsOu = $ConfXML.n.Admin.OUs.ItPrivGroupsOU.name # Build Distinguished Names [string]$ItAdminGroupsOuDn = ('OU={0},OU={1},{2}' -f $ItAdminGroupsOu, $ItAdminOu, $Variables.AdDn) [string]$ItRightsOuDn = ('OU={0},OU={1},{2}' -f $ItRightsOu, $ItAdminOu, $Variables.AdDn) [string]$ItPrivGroupsOuDn = ('OU={0},OU={1},{2}' -f $ItPrivGroupsOu, $ItAdminOu, $Variables.AdDn) } #end Begin Process { if ($PSCmdlet.ShouldProcess('Active Directory Privileged Groups', 'Create Tier0 Admin Groups')) { # Count total operations for progress tracking [int]$totalOperations = $confXML.n.Admin.LG.ChildNodes.Count + $confXML.n.Admin.GG.ChildNodes.Count + 4 # Additional operations for server groups [int]$currentOperation = 0 # Iterate through all Admin-LocalGroups child nodes Foreach ($Node in $confXML.n.Admin.LG.ChildNodes) { $currentOperation++ # Update progress $progressParams = @{ Activity = 'Creating Tier0 Admin Groups' Status = ('Creating group {0} ({1} of {2})' -f ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $Node.Name), $currentOperation, $totalOperations) PercentComplete = ($currentOperation / $totalOperations * 100) } Write-Progress @progressParams Write-Verbose -Message ('Create group {0}' -f ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $Node.LocalName)) $Splat = @{ Name = '{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $Node.Name GroupCategory = 'Security' GroupScope = 'DomainLocal' DisplayName = $Node.DisplayName Path = $ItRightsOuDn Description = $Node.Description ProtectFromAccidentalDeletion = $true RemoveAccountOperators = $true RemoveEveryone = $true RemovePreWin2000 = $true } $CreatedGroup = New-AdDelegatedGroup @Splat $VarParam = @{ Name = 'SL{0}{1}' -f $NC['Delim'], $Node.LocalName Value = $CreatedGroup Scope = 'Global' Force = $true } New-Variable @VarParam # Clear variable for next use $CreatedGroup = $null } #end ForEach # Iterate through all Admin-GlobalGroups child nodes Foreach ($Node in $confXML.n.Admin.GG.ChildNodes) { $currentOperation++ # Update progress $progressParams = @{ Activity = 'Creating Tier0 Admin Groups' Status = ('Creating group {0} ({1} of {2})' -f ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $Node.Name), $currentOperation, $totalOperations) PercentComplete = ($currentOperation / $totalOperations * 100) } Write-Progress @progressParams Write-Verbose -Message ('Create group {0}' -f ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $Node.localname)) $Splat = @{ Name = '{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $Node.Name GroupCategory = 'Security' GroupScope = 'Global' DisplayName = $Node.DisplayName Path = $ItAdminGroupsOuDn Description = $Node.Description ProtectFromAccidentalDeletion = $true RemoveAccountOperators = $true RemoveEveryone = $true RemovePreWin2000 = $true } $CreatedGroup = New-AdDelegatedGroup @Splat $VarParam = @{ Name = 'SG{0}{1}' -f $NC['Delim'], $Node.LocalName Value = $CreatedGroup Scope = 'Global' Force = $true } New-Variable @VarParam # Clear variable for next use $CreatedGroup = $null } #end ForEach # Create Servers Area / Tier1 Domain Local & Global Groups # Operations group $currentOperation++ # Update progress $progressParams = @{ Activity = 'Creating Tier0 Admin Groups' Status = ('Creating Operations group ({0} of {1})' -f $currentOperation, $totalOperations) PercentComplete = ($currentOperation / $totalOperations * 100) } Write-Progress @progressParams $Splat = @{ Name = '{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $ConfXML.n.Servers.GG.Operations.Name GroupCategory = 'Security' GroupScope = 'Global' DisplayName = $ConfXML.n.Servers.GG.Operations.DisplayName Path = $ItAdminGroupsOuDn Description = $ConfXML.n.Servers.GG.Operations.Description ProtectFromAccidentalDeletion = $true RemoveAccountOperators = $true RemoveEveryone = $true RemovePreWin2000 = $true } $CreatedGroup = New-AdDelegatedGroup @Splat $VariableName = 'SG{0}{1}' -f $NC['Delim'], $ConfXML.n.Servers.GG.Operations.LocalName New-Variable -Name $VariableName -Value $CreatedGroup -Scope Global -Force $CreatedGroup = $null # Server Admins group $currentOperation++ # Update progress $progressParams = @{ Activity = 'Creating Tier0 Admin Groups' Status = ('Creating Server Admins group ({0} of {1})' -f $currentOperation, $totalOperations) PercentComplete = ($currentOperation / $totalOperations * 100) } Write-Progress @progressParams $Splat = @{ Name = '{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $ConfXML.n.Servers.GG.ServerAdmins.Name GroupCategory = 'Security' GroupScope = 'Global' DisplayName = $ConfXML.n.Servers.GG.ServerAdmins.DisplayName Path = $ItAdminGroupsOuDn Description = $ConfXML.n.Servers.GG.ServerAdmins.Description ProtectFromAccidentalDeletion = $true RemoveAccountOperators = $true RemoveEveryone = $true RemovePreWin2000 = $true } $CreatedGroup = New-AdDelegatedGroup @Splat $VariableName = 'SG{0}{1}' -f $NC['Delim'], $ConfXML.n.Servers.GG.ServerAdmins.LocalName New-Variable -Name $VariableName -Value $CreatedGroup -Scope Global -Force $CreatedGroup = $null # Server Ops Rights group $currentOperation++ # Update progress $progressParams = @{ Activity = 'Creating Tier0 Admin Groups' Status = ('Creating Server Ops Rights group ({0} of {1})' -f $currentOperation, $totalOperations) PercentComplete = ($currentOperation / $totalOperations * 100) } Write-Progress @progressParams $Splat = @{ Name = '{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $ConfXML.n.Servers.LG.SvrOpsRight.Name GroupCategory = 'Security' GroupScope = 'DomainLocal' DisplayName = $ConfXML.n.Servers.LG.SvrOpsRight.DisplayName Path = $ItRightsOuDn Description = $ConfXML.n.Servers.LG.SvrOpsRight.Description ProtectFromAccidentalDeletion = $true RemoveAccountOperators = $true RemoveEveryone = $true RemovePreWin2000 = $true } $CreatedGroup = New-AdDelegatedGroup @Splat $VariableName = 'SL{0}{1}' -f $NC['Delim'], $ConfXML.n.Servers.LG.SvrOpsRight.LocalName New-Variable -Name $VariableName -Value $CreatedGroup -Scope Global -Force $CreatedGroup = $null # Server Admin Rights group $currentOperation++ # Update progress $progressParams = @{ Activity = 'Creating Tier0 Admin Groups' Status = ('Creating Server Admin Rights group ({0} of {1})' -f $currentOperation, $totalOperations) PercentComplete = ($currentOperation / $totalOperations * 100) } Write-Progress @progressParams $Splat = @{ Name = '{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $ConfXML.n.Servers.LG.SvrAdmRight.Name GroupCategory = 'Security' GroupScope = 'DomainLocal' DisplayName = $ConfXML.n.Servers.LG.SvrAdmRight.DisplayName Path = $ItRightsOuDn Description = $ConfXML.n.Servers.LG.SvrAdmRight.Description ProtectFromAccidentalDeletion = $true RemoveAccountOperators = $true RemoveEveryone = $true RemovePreWin2000 = $true } $CreatedGroup = New-AdDelegatedGroup @Splat $VariableName = 'SL{0}{1}' -f $NC['Delim'], $ConfXML.n.Servers.LG.SvrAdmRight.LocalName New-Variable -Name $VariableName -Value $CreatedGroup -Scope Global -Force $CreatedGroup = $null # Complete the group creation progress Write-Progress -Activity 'Creating Tier0 Admin Groups' -Completed # Get all Privileged groups into an array $AllGroups # Note: For each group we check if it exists before adding to the collection if ($null -ne $SG_InfraAdmins) { [void]$AllGroups.Add($SG_InfraAdmins) } if ($null -ne $SG_AdAdmins) { [void]$AllGroups.Add($SG_AdAdmins) } if ($null -ne $SG_Tier0ServiceAccount) { [void]$AllGroups.Add($SG_Tier0ServiceAccount) } if ($null -ne $SG_Tier1ServiceAccount) { [void]$AllGroups.Add($SG_Tier1ServiceAccount) } if ($null -ne $SG_Tier2ServiceAccount) { [void]$AllGroups.Add($SG_Tier2ServiceAccount) } if ($null -ne $SG_GpoAdmins) { [void]$AllGroups.Add($SG_GpoAdmins) } if ($null -ne $SG_Tier0Admins) { [void]$AllGroups.Add($SG_Tier0Admins) } if ($null -ne $SG_Tier1Admins) { [void]$AllGroups.Add($SG_Tier1Admins) } if ($null -ne $SG_Tier2Admins) { [void]$AllGroups.Add($SG_Tier2Admins) } if ($null -ne $SG_AllSiteAdmins) { [void]$AllGroups.Add($SG_AllSiteAdmins) } if ($null -ne $SG_AllGALAdmins) { [void]$AllGroups.Add($SG_AllGALAdmins) } # Move the groups to PG OU $totalGroups = $AllGroups.Count $currentGroup = 0 foreach ($Item in $AllGroups) { $currentGroup++ # Update progress for moving groups $progressParams = @{ Activity = 'Moving Privileged Groups to Protected OU' Status = ('Moving group {0} ({1} of {2})' -f $Item.Name, $currentGroup, $totalGroups) PercentComplete = ($currentGroup / $totalGroups * 100) } Write-Progress @progressParams try { # Remove the ProtectedFromAccidentalDeletion, otherwise throws error when moving Set-ADObject -Identity $Item.ObjectGUID -ProtectedFromAccidentalDeletion $false # Move objects to PG OU Move-ADObject -TargetPath $ItPrivGroupsOuDn -Identity $Item.ObjectGUID # Set back again the ProtectedFromAccidentalDeletion flag. # The group has to be fetched again because of the previous move Set-ADObject -Identity $Item.ObjectGUID -ProtectedFromAccidentalDeletion $true # Refresh the variable because DistinguishedName changed Set-Variable -Name $Item.SamAccountName -Value (Get-ADGroup -Identity $Item.SID) -Scope Global -Force # AD Object operations ONLY supports DN and GUID as identity Write-Verbose -Message ('Protect and Move group {0} to Privileged Groups OU. Update Variable.' -f $Item.Name) } catch { Write-Warning -Message ( 'Failed to update variable or move group {0}: {1}' -f $Item.Name, $_.Exception.Message ) } #end Try-Catch } #end foreach # Complete the group moving progress Write-Progress -Activity 'Moving Privileged Groups to Protected OU' -Completed } #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 Tier0 Admin Groups.' ) 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-Tier0AdminGroup |