Get certificate template effective permissions with PowerShell

In this article I will show the techniques used to determine effective permissions for a user or computer account on a certificate template.

The problem

When you open certificate template in Certificate Templates MMC snap-in (certtmpl.msc) or in ADSI Editor (adsiedit.msc) and switch to Security tab, you will see the following:

image

When you click Advanced button, you won’t see Effective Permissions tab like in NTFS permission editor:

image

If you have complex group structure with multiple-level nesting, it is not very easy to track effective permissions for an account. Also, this information might be very helpful when troubleshooting enrollment issues.

Solution

Solution will consist of several steps:

  1. Retrieve account membership information, including indirect group nesting;
  2. Retrieve certificate template ACL;
  3. Compare each account group permissions with ACL retrieved in step 2.

Looks easy, however actual code will do a lot of non-trivial work. And here we go:

Get account membership information

This step is fairly easy, we will use WindowsIdentity class:

PS C:\> New-Object Security.Principal.WindowsIdentity dc2@contoso.com


AuthenticationType : Kerberos
ImpersonationLevel : Identification
IsAuthenticated    : True
IsGuest            : False
IsSystem           : False
IsAnonymous        : False
Name               : CONTOSO\DC2$
Owner              : S-1-5-21-3709200118-438321133-4282490648-1120
User               : S-1-5-21-3709200118-438321133-4282490648-1120
Groups             : {S-1-5-21-3709200118-438321133-4282490648-515, S-1-1-0, S-1-5-32-554, S-1-5-32-561...}
Token              : 3296



PS C:\> (New-Object Security.Principal.WindowsIdentity dc2@contoso.com).Groups

                           BinaryLength AccountDomainSid                        Value
                           ------------ ----------------                        -----
                                     28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249...
                                     12                                         S-1-1-0
                                     16                                         S-1-5-32-554
                                     16                                         S-1-5-32-561
                                     16                                         S-1-5-32-545
                                     12                                         S-1-5-2
                                     12                                         S-1-5-11
                                     12                                         S-1-5-15
                                     28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249...
                                     28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249...
                                     28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249...
                                     28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249...


PS C:\>

Basically we use account’s user principal name (UPN) in the WindowsIdentity class constructor and then use Groups property to retrieve all groups account is member of. SID information doesn’t look very user-friendly. We can easily convert SID information to a NTAccount class:

PS C:\> $Groups = (New-Object Security.Principal.WindowsIdentity dc2@contoso.com).Groups | %{$_.translate([Security.Prin
cipal.NTAccount])}
PS C:\> $groups

Value
-----
CONTOSO\Domain Computers
Everyone
BUILTIN\Pre-Windows 2000 Compatible Access
BUILTIN\Terminal Server License Servers
BUILTIN\Users
NT AUTHORITY\NETWORK
NT AUTHORITY\Authenticated Users
NT AUTHORITY\This Organization
CONTOSO\CAs
CONTOSO\Web Servers
CONTOSO\CERTSVC_DCOM_ACCESS
CONTOSO\Cert Publishers


PS C:\>

Now it looks much better. If we look to the first figure and group list, we can tell (but not certainly) that specified computer account is a member of “Web Servers” group and this group has Read and Enroll permissions. However, there can be other groups with other permissions and even access type (Deny, instead of Allow) and what the hell, let PowerShell to do it’s work!

Just to note: although, it is a bad practice, some administrators assign permissions directly to a user/computer account, therefore, you may consider to add account name to the $Group variable:

$Groups += (New-Object Security.Principal.WindowsIdentity dc2@contoso.com).Name

Get certificate template ACL

This step is more interesting, because it requires to understand Active Directory permissions, which are much more complex, than NTFS or registry permissions. First, we need to retrieve certificate template object from Active Directory. Certificate templates are stored at:

CN=Certificate Templates, CN=Public Key Services, CN=Services, {Configuration naming context}

The following code is used to retrieve configuration naming context:

$ConfigContext = ([ADSI]"LDAP://RootDSE").configurationNamingContext

Prepend the path with certificate template container path and search for desired template:

$ConfigContext = ([ADSI]"LDAP://RootDSE").configurationNamingContext
$ConfigContext = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$ConfigContext"
# use this form to find template by common name
$filter = "(cn=WebServerV2)"
# use this for to find template by display name
$filter = "displayName=Web Server V2"
$ds = New-object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$ConfigContext",$filter)
$Template = $ds.Findone().GetDirectoryEntry() | ForEach-Object {$_}

Now, retrieve current ACL:

PS C:\> $Access = $Template.ObjectSecurity.Access
PS C:\> $access


ActiveDirectoryRights : GenericRead
InheritanceType       : None
ObjectType            : 00000000-0000-0000-0000-000000000000
InheritedObjectType   : 00000000-0000-0000-0000-000000000000
ObjectFlags           : None
AccessControlType     : Allow
IdentityReference     : NT AUTHORITY\Authenticated Users
IsInherited           : False
InheritanceFlags      : None
PropagationFlags      : None

ActiveDirectoryRights : CreateChild, DeleteChild, Self, WriteProperty, DeleteTree, Delete, GenericRead, WriteDacl, Writ
                        eOwner
InheritanceType       : None
ObjectType            : 00000000-0000-0000-0000-000000000000
InheritedObjectType   : 00000000-0000-0000-0000-000000000000
ObjectFlags           : None
AccessControlType     : Allow
IdentityReference     : CONTOSO\Domain Admins
IsInherited           : False
InheritanceFlags      : None
PropagationFlags      : None
<...>

and there we see a lot of ACEs (access control entry) there. Moreover, none of each ACE looks easy to understand. We need to take attention to the following permissions in the ActiveDirectoryRights property:

  • GenericRead = Read;
  • WriteDacl = Write;
  • GenericAll = Full Control
  • ExtendedRight = Enroll and/or Autoenroll.

Look here:

ActiveDirectoryRights : ExtendedRight
InheritanceType       : None
ObjectType            : 0e10c968-78fb-11d2-90d4-00c04f79dc55
InheritedObjectType   : 00000000-0000-0000-0000-000000000000
ObjectFlags           : ObjectAceTypePresent
AccessControlType     : Allow
IdentityReference     : CONTOSO\Web Servers
IsInherited           : False
InheritanceFlags      : None
PropagationFlags      : None

Here we see an ACE with ExtendedRight. This right represents either Enroll or Autoenroll permissions depending on ObjectType value:

  • 0e10c968-78fb-11d2-90d4-00c04f79dc55 = Enroll;
  • a05b8cc2-17bc-4802-a710-e7c15ab866a2 = Autoenroll.

So, we will grab and convert all rights to a common right list:

$Accesses = @()
$Accesses += $Template.ObjectSecurity.Access | %{
    $current = $_
    $Rights = @($current.ActiveDirectoryRights.ToString().Split(",",[StringSplitOptions]::RemoveEmptyEntries) | %{$_.trim()})
    $GUID = $current.ObjectType.ToString()
    $current | Add-Member -Name Permission -MemberType NoteProperty -Value @()
    if ($Rights -contains "GenericRead") {$current.Permission += "Read"}
    if ($Rights -contains "WriteDacl") {$current.Permission += "Write"}
    if ($Rights -contains "GenericAll") {$current.Permission += "Full Control"}
    if ($Rights -contains "ExtendedRight") {
        if ($GUID -eq "a05b8cc2-17bc-4802-a710-e7c15ab866a2") {$current.Permission += "Autoenroll"}
        elseif ($GUID -eq "0e10c968-78fb-11d2-90d4-00c04f79dc55") {$current.Permission += "Enroll"}
    }
    $current
}

In this code sample, we add a simple right name to each ACE, by translating native ACE to a simple right name. It will look like this:

Permission            : {Read, Write}
ActiveDirectoryRights : CreateChild, DeleteChild, Self, WriteProperty, DeleteTree, Delete, GenericRead, WriteDacl, Writ
                        eOwner
InheritanceType       : None
ObjectType            : 00000000-0000-0000-0000-000000000000
InheritedObjectType   : 00000000-0000-0000-0000-000000000000
ObjectFlags           : None
AccessControlType     : Allow
IdentityReference     : CONTOSO\Enterprise Admins
IsInherited           : False
InheritanceFlags      : None
PropagationFlags      : None

Permission            : {Enroll}
ActiveDirectoryRights : ReadProperty, WriteProperty, ExtendedRight
InheritanceType       : None
ObjectType            : 0e10c968-78fb-11d2-90d4-00c04f79dc55
InheritedObjectType   : 00000000-0000-0000-0000-000000000000
ObjectFlags           : ObjectAceTypePresent
AccessControlType     : Allow
IdentityReference     : CONTOSO\Domain Admins
IsInherited           : False
InheritanceFlags      : None
PropagationFlags      : None

I selected in bold custom added properties.

Filter ACL to get effective permissions

This step consist of several sub-steps:

  1. Retrieve all ACEs with Deny access type for each group account belongs to;
  2. Retrieval all ACEs with Allow access type for each group account belongs to;
  3. Retrieve effective Deny permissions;
  4. Retrieve effective Allow permissions;
  5. Compare effective Deny and Allow permissions and output only resulting Allow permissions.

Here we go:

# retrieve effective Deny permissions
$EffectiveDeny = $Accesses | Where-Object {$_.AccessControlType -eq "Deny"} | ForEach-Object {
    if ($Groups -contains $_.IdentityReference.ToString()) {
        $_.Permission
    }
}
# retrieve effective Allow permissions
$EffectiveAllow = $Accesses | Where-Object {$_.AccessControlType -eq "Allow"} | ForEach-Object {
    if ($Groups -contains $_.IdentityReference.ToString()) {
        $_.Permission
    }
}
# remove duplicates
$EffectiveDeny = $EffectiveDeny | Select-Object -Unique
$EffectiveAllow = $EffectiveAllow | Select-Object -Unique
# compare effective Deny and Allow permissions and output only Allow permissions
$EffectiveAllow | Where-Object {$EffectiveDeny -notcontains $_}

The last command will return effective permissions on a certificate template for a specified user or computer account:

PS C:\> $EffectiveDeny = $Accesses | Where-Object {$_.AccessControlType -eq "Deny"} | ForEach-Object {
>>     if ($Groups -contains $_.IdentityReference.ToString()) {
>>         $_.Permission
>>     }
>> }
>> # retrieve effective Allow permissions
>> $EffectiveAllow = $Accesses | Where-Object {$_.AccessControlType -eq "Allow"} | ForEach-Object {
>>     if ($Groups -contains $_.IdentityReference.ToString()) {
>>         $_.Permission
>>     }
>> }
>> # remove duplicates
>> $EffectiveDeny = $EffectiveDeny | Select-Object -Unique
>> $EffectiveAllow = $EffectiveAllow | Select-Object -Unique
>> # compare effective Deny and Allow permissions and output only Allow permissions
>> $EffectiveAllow | Where-Object {$EffectiveDeny -notcontains $_}
>>
Read
Enroll
PS C:\>

Hell ya! We got all we need! Probably, you want to wrap all the code to a simple function:

function Get-TemplateEffectivePermission {
[CmdletBinding(DefaultParameterSetName="Name")]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = "Name")]
        [string]$Name,
        [Parameter(Mandatory = $true, ParameterSetName = "DisplayName")]
        [string]$DisplayName,
        [string]$UPN
    )
    if ($UPN) {
        $User = New-Object Security.Principal.WindowsIdentity -ArgumentList $UPN
    } else {
        $User = [Security.Principal.WindowsIdentity]::GetCurrent()
    }
    $IDs = $User.Groups | %{$_.Translate([Security.Principal.NTAccount])}
    $IDs += $User.Name
    $filter = switch ($PsCmdlet.ParameterSetName) {
        "Name" {"(cn=$Name)"}
        "DisplayName" {"displayName=$DisplayName"}
    }
    $ConfigContext = ([ADSI]"LDAP://RootDSE").configurationNamingContext
    $ConfigContext = "CN=Certificate Templates,CN=Public Key Services,CN=Services,"
    $ds = New-object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$ConfigContext",$filter)
    $Template = $ds.Findone().GetDirectoryEntry() | %{$_}
    $Accesses = @()
    $Accesses += $Template.ObjectSecurity.Access | %{
        $current = $_
        $Rights = @($current.ActiveDirectoryRights.ToString().Split(",",[StringSplitOptions]::RemoveEmptyEntries) | %{$_.trim()})
        $GUID = $current.ObjectType.ToString()
        $current | Add-Member -Name Permission -MemberType NoteProperty -Value @()
        if ($Rights -contains "GenericRead") {$current.Permission += "Read"}
        if ($Rights -contains "WriteDacl") {$current.Permission += "Write"}
        if ($Rights -contains "GenericAll") {$current.Permission += "Full Control"}
        if ($Rights -contains "ExtendedRight") {
            if ($GUID -eq "a05b8cc2-17bc-4802-a710-e7c15ab866a2") {$current.Permission += "Autoenroll"}
            elseif ($GUID -eq "0e10c968-78fb-11d2-90d4-00c04f79dc55") {$current.Permission += "Enroll"}
        }
        $current
    }
    $EffectiveDeny = $Accesses | Where-Object {$_.AccessControlType -eq "Deny"} | ForEach-Object {
        if ($IDs -contains $_.IdentityReference.ToString()) {
            $_.Permission
        }
    }
    $EffectiveAllow = $Accesses | Where-Object {$_.AccessControlType -eq "Allow"} | ForEach-Object {
        if ($IDs -contains $_.IdentityReference.ToString()) {
            $_.Permission
        }
    }
    $EffectiveDeny = $EffectiveDeny | Select-Object -Unique
    $EffectiveAllow = $EffectiveAllow | Select-Object -Unique
    $EffectiveAllow | Where-Object {$EffectiveDeny -notcontains $_}    
}

The function accepts two arguments: certificate template common or display name and account’s user principal name. If UPN is not specified, current user account is used instead:

PS C:\> Get-TemplateEffectivePermission -Name webserverv2 -UPN dc2@contoso.com
Read
Enroll
PS C:\> Get-TemplateEffectivePermission -Name webserverv2
Read
Write
Enroll
PS C:\>

the task is solved!

HTH

Comments:

Tim
Tim 12.11.2015 23:57 (GMT+2)

Vadims,

I needed to change these two lines to get it to work. here are the new lines:

$ConfigContext = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$ConfigContext"

$ds = New-object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$ConfigContext",$filter)

Let me know if the above changes improperly altered your script. -Tim.

Tim
Tim 13.11.2015 01:19 (GMT+2)

To more accurately explain...I had to correct the full function code at the bottom of the article, but the lines were correct in your part by part explaination earlier in the article. 

Captcha