Following the Housekeeping series, we provide an idea on how to use Powershell to automate the provisioning of a new Semi-Privileged user.
OK, before we start, some background of Semi-Privileged user. The whole idea is to have the absolute least of Privileged users. A Privileged user is any user who is member of Administrators, Domain Admins, Enterprise Admins, Schema Admins, etc.
Indeed desirable, but somehow difficult if no Delegation Model exists. Let’s assume you have made your homework, and that the model is already implemented. Now we can provision a semi-privileged user. Remember, this user must not be part of any of the privileged groups mentioned before.
As we put in place Segregation of Duties, first of all a standard user account must exist. We are using this account as the “driver” of Semi-Privileged ones. In other words, if the standard account is lock out or disabled, then any semi-privileged account must be disabled immediately. If we delete this account, then the other related accounts have no reason to exist.
Interesting part here, is how to “link” the standard (non-privileged) user to the Semi-Privileged accounts. There are many ways to achieve this, but we decide to use an existing AD attribute. Every AD user has an attribute called “employeeNumber”. This attribute is in use for storing the company identification number of the user. As we are provisioning “administration purpose” users, then this attribute can contain the SID of the standard user. By storing the SID here, we are effectively creating a “link” between accounts.
Rules for Semi-Privileged user provisioning
The following Powershell code will create the Semi-Privileged user taking into consideration:
- Standard user (non-Privileged user) exists
- Create or Update Semi-Privileged user
- Send UserID and password by Email
Lets get some variables. We will use them at a latter time.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$EmailEncoding='ASCII' # Get the DNS name of current domain $AdDomain = (Get-ADDomain).DNSRoot # Flag indicating if email is to be sent $SendEmail = $false #Get the new random password $unsecurePassword = Get-RandomPassword -Length 15 -Complexity High # Convert new random password into a SecureString $setpass = ConvertTo-SecureString -AsPlainText $unsecurePassword -force |
Next step is to retrieve the standard user and all its properties, so we can transform those for the new one. Here we will “try and catch” 2 exceptions:
- Ad Identity Not Found (ADIdentityNotFoundException)
- AD Identity Already Exists (ADIdentityAlreadyExistException)
When we enclose actions within try-catch block, we can catch specific exceptions and manage them accordingly. In this case, when the standard user is not found, we immediately stop. Remember, if no standard user exist, then we cannot have a semi-privileged user. The second exception indicates that the Semi-Privileged user already exists. So instead of “creating” we just “change it”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
Try { # Define all properties we want $properties = 'City', 'Company', 'Country', 'Department', 'Description', 'Division', 'EmployeeId', 'EmployeeNumber', 'GivenName', 'MobilePhone', 'Office', 'OfficePhone', 'Organization', 'OtherName', 'POBox', 'PostalCode', 'SID', 'State', 'StreetAddress', 'Surname', 'Title', 'EmailAddress' # Get user and all its properties $StdUser = Get-ADUser -Identity $SamAccountName -Properties $properties # Prepara all data for the new Semi-Privileged user $parameters = @{ SamAccountName = '{0}_{1}' -f $SamAccountName, $AccountType UserPrincipalName = '{0}_{1}@{2}' -f $SamAccountName, $AccountType, $AdDomain Name = '{0}, {1} ({2})' -f $StdUser.Surname.ToUpper(), (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()), $AccountType DisplayName = '{0}, {1} ({2})' -f $StdUser.Surname.ToUpper(), (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()), $AccountType Surname = $StdUser.Surname.ToUpper() GivenName = (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()) Path = 'OU=USER-ACCOUNTS,OU=ADMINISTRATION,{0}' -f (Get-ADDomain).DistinguishedName Enabled = $true TrustedForDelegation = $false AccountNotDelegated = $true ChangePasswordAtLogon = $false ScriptPath = $null HomeDrive = $null HomeDirectory = $null AccountPassword = $setpass City = $StdUser.City Company = $StdUser.Company Country = $StdUser.Country Department = $StdUser.Department Description = '{0} Admin account' -f $AccountType Division = $StdUser.Division EmailAddress = $StdUser.EmailAddress EmployeeId = $StdUser.EmployeeId EmployeeNumber = $StdUser.SID.Value.ToString() MobilePhone = $StdUser.MobilePhone Office = $StdUser.Office OfficePhone = $StdUser.OfficePhone Organization = $StdUser.Organization OtherName = $StdUser.OtherName POBox = $StdUser.POBox PostalCode = $StdUser.PostalCode State = $StdUser.State StreetAddress = $StdUser.StreetAddress Title = $StdUser.Title OtherAttributes = @{'employeeType'=$AccountType; 'msNpAllowDialin'=$false; 'msDS-SupportedEncryptionTypes'=24} } New-ADUser @parameters Write-Verbose -Message ('New Admin Account {0} of type {1} was created correctly.' -f $SamAccountName, $AccountType) $SendEmail = $True } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { Write-Warning "No Standard user exist. Some information might be missing in order to create the new object." $SendEmail = $false } catch [Microsoft.ActiveDirectory.Management.ADIdentityAlreadyExistsException] { $parameters = @{ Identity = '{0}_{1}' -f $SamAccountName, $AccountType SamAccountName = '{0}_{1}' -f $SamAccountName, $AccountType UserPrincipalName = '{0}_{1}@{2}' -f $SamAccountName, $AccountType, $AdDomain DisplayName = '{0}, {1} ({2})' -f $StdUser.Surname.ToUpper(), (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()), $AccountType Surname = $StdUser.Surname.ToUpper() GivenName = (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()) Enabled = $true TrustedForDelegation = $false AccountNotDelegated = $true ChangePasswordAtLogon = $false ScriptPath = $null HomeDrive = $null HomeDirectory = $null City = $StdUser.City Company = $StdUser.Company Country = $StdUser.Country Department = $StdUser.Department Description = '{0} Admin account' -f $AccountType Division = $StdUser.Division EmailAddress = $StdUser.EmailAddress EmployeeId = $StdUser.EmployeeId EmployeeNumber = $StdUser.SID.Value.ToString() MobilePhone = $StdUser.MobilePhone Office = $StdUser.Office OfficePhone = $StdUser.OfficePhone Organization = $StdUser.Organization OtherName = $StdUser.OtherName POBox = $StdUser.POBox PostalCode = $StdUser.PostalCode State = $StdUser.State StreetAddress = $StdUser.StreetAddress Title = $StdUser.Title Add = @{'employeeType'=$AccountType; 'msNpAllowDialin'=$false; 'msDS-SupportedEncryptionTypes'=24} } Set-ADUser @parameters Write-Verbose "Existing Admin Account $SamAccountName of type $AccountType was modified accordingly." $SendEmail = $false } catch { $error } |
Important attributes to bear in mind
TrustedForDelegation: Security account delegation provides the ability to connect to multiple servers, and each server change retains the authentication credentials of the original client. Delegation of authentication is a capability that client and server applications use when they have multiple tiers. It allows a public-facing service to use client credentials to authenticate to an application or database service. Is very rare that this is needed, and for sure not on our Semi-Privileged users.
AccountNotDelegated: Credentials can not be reused by a trusted service. This limits the scope of attacks that use delegation, e.g. elevation of privilege activities. Always enable this setting.
EmployeeNumber: We use this attribute to “link” this account to the non-privileged account (standard user). For example, if we need to reset the password, we will use the standard user email to send the new password of this Semi-Privileged account.
employeeType: We use this attribute to “tag” the semi-privileged user. For example, if this account is a Tier0 account, then we will store T0 as a tag.
msNpAllowDialin: Indicates whether the account has permission to dial in to the RAS server. We don’t want this kind of access in our Semi-Privileged account.
msDS-SupportedEncryptionTypes: Support for newer and more secure algorithm AES (RFC3962) (128 AND 256). Used by KDC to encrypt the corresponding ticket. A must in our accounts.
Powershell Semi-Privileged user provisioning Function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
Function New-SemiPrivilegedUser { <# .Synopsis Create new Semi-Privileged user .DESCRIPTION Create new Semi-Privileged user by following security standards. A non-privileged user must exist in order to create the user. .EXAMPLE New-SemiPrivilegedUser "dvader" "darth.vader@EguibarIT.com" "T0" New-SemiPrivilegedUser -SamAccountName "dvader" -Emailto "darth.vader@EguibarIT.com" -AccountType "T0" .INPUTS Param1 SamAccountName...: [String] SamAccountName or Bensel or UserID of the new admin account to be created. Usually a existing user ID. Param2 Emailto..........: [String] Valid Email of the target user. This address will be used to send information to her/him. Param3 AccountType......: [String] Must specify the account type. Valid values are T0 or T1 or T2 Param4 EmailServer......: [String] SMTP server used to send the email .NOTES Version: 1.0 DateModified: 29/Jul/2017 LasModifiedBy: Vicente Rodriguez Eguibar vicente@eguibar.com Eguibar Information Technology S.L. http://www.eguibarit.com #> <# EGUIBARIT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT. AS TO DOCUMENTS AND CODE, EGUIBARIT MAKES NO REPRESENTATION OR WARRANTY THAT THE CONTENTS OF SUCH DOCUMENT OR CODE ARE FREE FROM ERROR OR SUITABLE FOR ANY PURPOSE; NOR THAT IMPLEMENTATION OF SUCH CONTENTS WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS., provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software product in which the Code is embedded; (ii) to include a valid copyright notice on Your software product in which the Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims or lawsuits, including attorneys' fees, that arise or result from the use or distribution of the Code. This posting is provided "AS IS" with no warranties, and confers no rights. Use of included script are subject to the terms specified at http://eguibarit.eu/copyright-notice-and-disclaimers/ #> [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] Param( #Param1 [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, HelpMessage='SamAccountName or UserID of the new admin account to be created. Must be a existing user ID.', Position=0)] [ValidateNotNullOrEmpty()] [String] $SamAccountName, #Param2 [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, HelpMessage='Valid Email of the target user. This address will be used to send information to her/him.', Position=1)] [ValidateNotNullOrEmpty()] [ValidatePattern("^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$")] [String[]] $Emailto, #Param3 [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, HelpMessage='Must specify the account type. Valid values are T0 or T1 or T2', Position=2)] [ValidateNotNullOrEmpty()] [ValidateSet('T0', 'T1', 'T2')] [String] $AccountType, #Param4 [Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, HelpMessage='SMTP server used to send the email', Position=3)] [String] $EmailServer = 'smtp.internal.EguibarIT.local' ) Begin { Import-Module EguibarIT Import-Module ActiveDirectory $Emailfrom = 'DelegationModel@{0}' -f $env:userDnsDomain $EmailTextPath = 'C:\PsScripts\DelegationModel\Email-MessageHTML.txt' $SendEmail = $false ######################################################################################################################################### # DO NOT MODYFY ANYTHING BELOW THIS POINT. ######################################################################################################################################### $EmailEncoding='ASCII' $AdDomain = (Get-ADDomain).DNSRoot $unsecurePassword = Get-RandomPassword -Length 15 -Complexity High $setpass = ConvertTo-SecureString -AsPlainText $unsecurePassword -force } Process { Try { $properties = 'City', 'Company', 'Country', 'Department', 'Description', 'Division', 'EmployeeId', 'EmployeeNumber', 'GivenName', 'MobilePhone', 'Office', 'OfficePhone', 'Organization', 'OtherName', 'POBox', 'PostalCode', 'SID', 'State', 'StreetAddress', 'Surname', 'Title', 'EmailAddress' $StdUser = Get-ADUser -Identity $SamAccountName -Properties $properties $parameters = @{ SamAccountName = '{0}_{1}' -f $SamAccountName, $AccountType UserPrincipalName = '{0}_{1}@{2}' -f $SamAccountName, $AccountType, $AdDomain Name = '{0}, {1} ({2})' -f $StdUser.Surname.ToUpper(), (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()), $AccountType DisplayName = '{0}, {1} ({2})' -f $StdUser.Surname.ToUpper(), (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()), $AccountType Surname = $StdUser.Surname.ToUpper() GivenName = (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()) Path = 'OU=USER-ACCOUNTS,OU=ADMINISTRATION,{0}' -f (Get-ADDomain).DistinguishedName Enabled = $true TrustedForDelegation = $false AccountNotDelegated = $true ChangePasswordAtLogon = $false ScriptPath = $null HomeDrive = $null HomeDirectory = $null AccountPassword = $setpass City = $StdUser.City Company = $StdUser.Company Country = $StdUser.Country Department = $StdUser.Department Description = '{0} Admin account' -f $AccountType Division = $StdUser.Division EmailAddress = $StdUser.EmailAddress EmployeeId = $StdUser.EmployeeId EmployeeNumber = $StdUser.SID.Value.ToString() MobilePhone = $StdUser.MobilePhone Office = $StdUser.Office OfficePhone = $StdUser.OfficePhone Organization = $StdUser.Organization OtherName = $StdUser.OtherName POBox = $StdUser.POBox PostalCode = $StdUser.PostalCode State = $StdUser.State StreetAddress = $StdUser.StreetAddress Title = $StdUser.Title OtherAttributes = @{'employeeType'=$AccountType; 'msNpAllowDialin'=$false; 'msDS-SupportedEncryptionTypes'=24} } New-ADUser @parameters Write-Verbose -Message ('New Admin Account {0} of type {1} was created correctly.' -f $SamAccountName, $AccountType) $SendEmail = $True } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { Write-Warning "No Standard user exist. Some information might be missing in order to create the new object." $SendEmail = $false } catch [Microsoft.ActiveDirectory.Management.ADIdentityAlreadyExistsException] { $parameters = @{ Identity = '{0}_{1}' -f $SamAccountName, $AccountType SamAccountName = '{0}_{1}' -f $SamAccountName, $AccountType UserPrincipalName = '{0}_{1}@{2}' -f $SamAccountName, $AccountType, $AdDomain DisplayName = '{0}, {1} ({2})' -f $StdUser.Surname.ToUpper(), (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()), $AccountType Surname = $StdUser.Surname.ToUpper() GivenName = (Get-Culture).TextInfo.ToTitleCase($StdUser.GivenName.ToLower()) Enabled = $true TrustedForDelegation = $false AccountNotDelegated = $true ChangePasswordAtLogon = $false ScriptPath = $null HomeDrive = $null HomeDirectory = $null City = $StdUser.City Company = $StdUser.Company Country = $StdUser.Country Department = $StdUser.Department Description = '{0} Admin account' -f $AccountType Division = $StdUser.Division EmailAddress = $StdUser.EmailAddress EmployeeId = $StdUser.EmployeeId EmployeeNumber = $StdUser.SID.Value.ToString() MobilePhone = $StdUser.MobilePhone Office = $StdUser.Office OfficePhone = $StdUser.OfficePhone Organization = $StdUser.Organization OtherName = $StdUser.OtherName POBox = $StdUser.POBox PostalCode = $StdUser.PostalCode State = $StdUser.State StreetAddress = $StdUser.StreetAddress Title = $StdUser.Title Add = @{'employeeType'=$AccountType; 'msNpAllowDialin'=$false; 'msDS-SupportedEncryptionTypes'=24} } try { Set-ADUser @parameters Write-Verbose "Existing Admin Account $SamAccountName of type $AccountType was modified accordingly." $SendEmail = $false } catch{ $error } } catch { $error } Try { If($SendEmail) { ########################################################################################################### #region Send "User was created" information Email Write-Verbose -Message "[PROCESS] Preparing the notification email..." $body = Get-Content -Path $EmailTextPath $body = $body -replace '#DomainName#', $($AdDomain) $body = $body -replace '#UserID#', ($SamAccountName + '_' + $AccountType) $body = $body -replace '#unsecurePassword#', $unsecurePassword # Preparing the Email properties $SmtpClient = New-Object -TypeName system.net.mail.smtpClient $SmtpClient.host = $EmailServer $MailMessage = New-Object -TypeName system.net.mail.mailmessage $MailMessage.from = $EmailFrom FOREACH ($To in $Emailto) { $MailMessage.To.add($($To)) } $MailMessage.IsBodyHtml = $true $MailMessage.Subject = 'New Administrative account based on the AD Delegation Model' $MailMessage.Body = $Body # Encoding $MailMessage.BodyEncoding = [System.Text.Encoding]::$EmailEncoding $MailMessage.SubjectEncoding = [System.Text.Encoding]::$EmailEncoding # Sending the Email $SmtpClient.Send($MailMessage) } } catch{ $error; $SendEmail; $SendPwd = $False } } End { } } |
Final thoughts
Use this function in your test environment first. Make sure it will do exactly what you are looking for.
Provide a text file containing your HTML code (looks nicer!). Don’t forget to include #UserID#, #DomainName# and #unsecurePassword# placeholders. These will replace by the current data.
Some corporate policies forbid to send password over email. This is because non encrypted email is easy to read. If this is your case, this example is missing an email encryption section. Encrypting email will depend on your PKI infrastructure and Email application in use.