Powershell Semi-Privileged user provisioning

Spread the love

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.

SemiPrivileged_overview
SemiPrivileged users and roles distribution. Advanced alternative to Microsoft model.

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.

$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”.

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


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.