CM Configuration Using PowerShell

    General discussion

  • The Certificate Management (CM) feature of ILM2007 and FIM 2010 squeezes a lot of functionality from the Windows platform. Installing and configuring CM together with the management agent for CM can be challenging, and this article details PowerShell scripts to assist in CM configuration.


    The scripts contained in this document were inspired by the script introduced by Markus Vilcinskas in the ILM 2007 Getting Started Guide named Introduction to the Management Agent for Certificate and Smart Card Management.
    That guide provides an exhaustive series of steps for installing and configuring CM, and the script from the appendix verifies the configuration.

    Troubleshooting CM configurations can be tricky, so this article breaks down the script into smaller components to further assist in CM configuration.
    I have used these scripts to stand up CLM lab environments in minutes instead of hours.
    The scripts contained here will be useful if you are looking for examples for using PowerShell with CM.

    Why did I do this?

    Beyond inspiration from Markus, I found myself having to stand up CLM environments in a consistent manner, over and over again.
    The documented checklist became tiresome and error prone so I began to apply as much automation as I could.
    These scripts have saved me loads of time, allowing me to blow off work early to spend time falling off my bike and getting up again.
    One could argue the productivity gained or lost but hopefully these scripts will save you as much time as they did me.

    Found Bugs? Got Feedback?

    A static document serves as a great way to introduce scripts, but does not perform well as the scripts change over time.
    Therefore the scripts can also be found on the MSDN Code Gallery.
    Please provide feedback regarding the script code and report issues MSDN Code Gallery.

    Updating the CM Web.Config File


    The management agent for CM requires an entry in CM’s web.config file.
    This script locates the web.config file then updates it with the required entries.

    Why bother finding and updating the CM Web.Config file?
    The management agent for CM uses .NET Remoting to communicate with CM, and since CM is mostly a web service it is configured through its Web.Config file.
    This is very similar to the approach for using NET Framework Remoting in the Provision API as described in MSDN.

    Before the management agent for CM can call into the CM service it first needs to add an endpoint to the Web.Config file.
    If you are not using the management agent for CM then this may not be relevant, but the script is still useful as a sample if you want to use PowerShell to adjust any settings in the CM Web.Config file because it illustrates how to:

    1. Find the CM Web.Config file using the registry
    2. Insert XML nodes into the Web.Config file
    3. Validate XML nodes in the Web.Config file

    The path to the CM web.config file is found in the registry on the server where CM is installed.
    Running this script on a server where CM is not installed will not work.


    Function ModifyClmWebConfig 
    { Write-Host Write-Host "Updating the CLM Server Web.Config File..." -ForegroundColor Green $regPath = "HKLM:\SOFTWARE\Microsoft\CLM\v1.0\Setup" $fileName = (Get-ItemProperty -path $regPath).WebFolder + "Web.Config" ### ### Find the path to CLM ### $binFolder = (Get-ItemProperty -path $regPath).WebFolder + "Bin" Write-host " CLM Web Bin: " $binFolder Write-host " CLM Web.Config File: " $fileName [xml]$xmlDoc = get-content $fileName ### ### Insert the node for the CLM MA Proxy ### if($xmlDoc.selectNodes(
    "//service/wellknown[@type='ExtensibleWfMA.ClmMaProxy,Microsoft.Clm.ClmMaProxy']").count -eq 0)
    { $xmlNode = [xml]"" $newNode = $xmlDoc.ImportNode($xmlNode.get_DocumentElement(), $true) $xmlDoc.get_DocumentElement().AppendChild($newNode) $xmlDoc.selectSingleNode("//service").AppendChild($newNode) } ### ### Check the node for the CLM MA Proxy ###

    "//service/wellknown[@type='ExtensibleWfMA.ClmMaProxy,Microsoft.Clm.ClmMaProxy']").count -eq 0)
    { Write-Host " Failed to insert node for CLM MA Proxy." -ForegroundColor Red $node1Added = $false } else
    { Write-Host " Node for CLM MA Proxy inserted." -ForegroundColor Green $node1Added = $true } Write-Host if ($node1Added)
    { $xmlDoc.save($fileName) return $true } else
    { return $false } } ModifyClmWebConfig


    Applying Permissions on the CM Service Connection Point

    Through schema extension, CM adds a set of extended permissions to Active Directory to allow certificate managers to delegate management activities.
    For more information about extended permissions, see Installing and Configuring ILM CMS on a Server.
    Permissions are required on the service connection point for CM.
    This script finds the service connection point in Active Directory then applies the CM permissions for the ‘Managers’ group.
    The name of the group is hard coded in the script below, and should be changed to match your environment.
    Also note the hard coded permission GUIDs in the SDDL below.
    Those are the GUIDs of the extended permissions created when CM is installed, and will be consistent across environments unless the CM product changes these GUIDs which is unlikely.
    The GUIDs do not need to be changed to match your environment.
    The extended permissions set by this script include:

    • CLMRequestRevoke
    • CLMAudit
    • CLMRequestRenew
    • CLMEnrollmentAgent
    • CLMRequestEnroll
    • CLMRequestRecover
    • CLMRequestUnblockSmartCard
    ### Get the SID of the ClmManagers Group
    ### It will be used to construct the SDDL below
    $clmManagersSid = .\Get-Sid.ps1 clmmanagers
    ### Construct SDDL strings using the CLM permission GUIDs and the SID of the ClmManagers group
    ###		Issue: We depend on the CLM permission GUIDs to be consistent across environments
    $CLMRequestRevoke = '(OA;;CR;a0028c0a-611c-4b1c-a05f-8bf48c80fdea;;' + $clmManagersSid + ')'
    $CLMAudit = '(OA;;CR;e2b76519-c5ef-4591-8c12-e185bce679f5;;' + $clmManagersSid + ')'
    $CLMRequestRenew = '(OA;;CR;0229c639-f0bf-4fc9-a238-6818aa18ed48;;' + $clmManagersSid + ')'
    $CLMEnrollmentAgent = '(OA;;CR;40dd7f48-eea5-4793-a09d-a3e458d81c18;;' + $clmManagersSid + ')'
    $CLMRequestEnroll = '(OA;;CR;0fd2ab9d-155e-4380-bbb7-08a8bda0e294;;' + $clmManagersSid + ')'
    $CLMRequestRecover = '(OA;;CR;b487f8f1-bfdb-4d04-a8da-122308d88b79;;' + $clmManagersSid + ')'
    $CLMRequestUnblockSmartCard = '(OA;;CR;a43837f9-b8f3-40c0-9a19-28a1f0d851c8;;' + $clmManagersSid + ')'
    ### ### Find the CLM Service Connection Point ### $objSearcher = New-Object System.DirectoryServices.DirectorySearcher $objSearcher.SearchRoot = [ADSI]'' $objSearcher.SearchScope = "Subtree" $objSearcher.PageSize = 1000 $objSearcher.Filter = "(objectClass=msClm-ServiceConnectionPoint)" $scpResults = $objSearcher.FindAll()
    foreach ($scp in $scpResults) { ### ### Apply permissions for the ClmUsers and ClmManagers Groups
    ### $scpEntry = $scp.GetDirectoryEntry() ### ### Get the ACL from the ClmUsers Group in SDDL form ### $currentACL =
    [System.Security.AccessControl.AccessControlSections]::All) ### ### Add our new ACE's to the current ACL ### $newACL = $currentACL + $CLMRequestRevoke + $CLMAudit + $CLMRequestRenew
    + $CLMEnrollmentAgent + $CLMRequestEnroll
    + $CLMRequestRecover + $CLMRequestUnblockSmartCard ### ### Set the new ACL on the ClmUsers group ### $scpEntry.psbase.ObjectSecurity.SetSecurityDescriptorSddlForm($newACL) $scpEntry.psbase.commitchanges() Write-host "Permissions applied to SCP: " + $scpEntry.Name -fore green }

    Applying CM Registry Permissions

    The management agent for CM requires specific registry permissions to:

    1. Write items to the Event Log
    2. Find the CM Web.Config file

    The script below applies those permissions to the registry.
    The CM registry entries only exist on the server where CM is installed.
    Running this script on a server where CM is not installed will not work.

    Function ModifyClmUserRegAcls 
    { param ([String]$clmUser)
    Write-Host Write-Host "Updating the registry permissions..." -ForegroundColor Green if (ValidateUser($clmuser))
    { write-host " Setting Registry Permissions for: " -Foregroundcolor Green -NoNewLine write-host $clmuser SetAclOnRegKey "HKLM:\Software\Microsoft\CLM\v1.0\server\WebUser" $clmUser "ReadKey" "Allow" SetAclOnRegKey "HKLM:\Software\Microsoft\CLM\v1.0\Setup" $clmUser "ReadKey" "Allow" } } Function ModifyILMuserRegAcls { param ([String]$ilmUser) if (ValidateUser($ilmUser))
    { write-host " Setting Registry Permissions for: " -Foregroundcolor Green -NoNewLine write-host $ilmUser SetAclOnRegKey "HKLM:\System\CurrentControlSet\Services\EventLog" $ilmUser "FullControl" "Allow" SetAclOnRegKey "HKLM:\System\CurrentControlSet\Services\EventLog\Security" $ilmUser "FullControl" "Allow" SetAclOnRegKey "HKLM:\Software\Microsoft\EnterpriseCertificates" $ilmUser "ReadKey" "Allow" SetAclOnRegKey "HKLM:\Software\Policies\Microsoft\SystemCertificates" $ilmUser "ReadKey" "Allow" SetAclOnRegKey "HKCU:\Software\Policies\Microsoft\systemCertificates" $ilmUser "ReadKey" "Allow" SetAclOnRegKey "HKCU:\Software\Microsoft\SystemCertificates" $ilmUser "ReadKey" "Allow" } } Function ModifySvcUseRegAcls
    { param ([String]$svcUser) if (ValidateUser($svcUser))
    { SetAclOnRegKey "HKLM:\Software\Policies\Microsoft\SystemCertificates" $svcUser "ReadKey" "Allow" } } Function SetAclOnRegKey
    { param([String]$regKey,[String]$user,[String]$perms,[String]$category) $acls = get-acl $regKey if ($acls -ne $null)
    { $newAcl = new-object System.Security.AccessControl.RegistryAccessRule($user,$perms,"ContainerInherit","None",$category) $acls.setAccessRule($newAcl) set-acl -path $regKey -aclObject $acls write-host " Registry permission " -Foregroundcolor Green -NoNewLine write-host $category -NoNewLine write-host ":" $perms -NoNewLine write-host " on " -Foregroundcolor Green -NoNewLine write-host $regKey } else
    { write-host "Error in getting ACLs from Registry Key -> $regKey" Exit } } Function ValidateUser
    { param ([String]$user) $account = new-object System.Security.Principal.NtAccount($user) $ErrorActionPreference = "SilentlyContinue" $guid = $null
    $guid = $account.translate([system.security.principal.securityidentifier]) $ErrorActionPreference = "Continue" if ($guid -eq $null)
    { Write-Host " User Account Error -> $account does not exist
    and cannot be resolved into a security principal"
    -ForegroundColor Red Exit } else
    { Write-host " Found security principal for: " $user -ForegroundColor Green return $true } return $false } ModifyClmUserRegAcls (${env:UserDomain} + "\clmma") ModifyILMuserRegAcls (${env:UserDomain} + "\ilmsvc") ModifySvcUseRegAcls (${env:UserDomain} + "\ilmsvc")

    Applying CM Database Permissions

    The management agent for CM reads objects from the CM database during imports.
    Read access is required on that database, otherwise the management agent will fail when imports are run.

    The script below creates a SQL Login mapped to a Windows account for the CM management agent then assigns it the ‘db_datareader’ role.
    The TODO’s in the script show that I was intending on discovering the CM database server instead of hard coding it but for now the script must be run on the server hosting the CM database.

    ### Add the CLMMA Account to the 'db_datareader' role in the CLM Database
    # Create the SQL Login
    $clmma = ${env:userdomain} + "\clmma"

    #TODO: if exists... #TODO: find the database from CLM instead of assuming local & osql -S localhost -E -Q "IF EXISTS (SELECT * FROM sys.server_principals
    WHERE name = N'$clmma') DROP LOGIN [$clmma]"
    & osql -S localhost -E -Q "CREATE LOGIN [$clmma] FROM WINDOWS;" # # Create the SQL User in the CLM Database # & osql -S localhost -E -Q "Use CLM;IF EXISTS (SELECT * FROM sys.database_principals
    WHERE name = '$clmma') DROP USER [$clmma]"
    & osql -S localhost -E -Q "use CLM;CREATE USER [$clmma] FOR LOGIN [$clmma];" # # Assign the User to the Role # & osql -S localhost -E -Q "use CLM;exec sp_addrolemember 'db_datareader', '$clmma'"

    Applying CM Profile Template Permissions in Active Directory

    Profile Template objects in CM are stored as directory objects in Active Directory.
    Permissions must be applied on those directory objects.

    The two scripts below enumerate the profile template objects then apply the CM extended permission for ‘Enroll’ and the Windows permission for ‘Read’.
    These permissions are visible when browsing the directory either with LDP, ADSIEdit or the Sites and Services Snap-In.

    Set-ClmProfileTemplateAdPermissions.ps1 Script

    ### Get the SID for the ClmUsers and ClmManagers groups
    $clmUsersSid = .\Get-Sid.ps1 clmusers
    $clmManagersSid = .\Get-Sid.ps1 clmmanagers
    ### Find the Configuration Partition
    $configDN = ([ADSI]'LDAP://rootdse').configurationnamingcontext
    $config = [ADSI]("LDAP://" + $configDN)
    ### Find the Profile Template Objects
    $objSearcher = New-Object System.DirectoryServices.DirectorySearcher
    $objSearcher.SearchRoot = $config
    $objSearcher.SearchScope = "Subtree" 
    $objSearcher.PageSize = 1000 
    $objSearcher.Filter = "objectClass=msClm-ProfileTemplate"
    $profileTemplates = $objSearcher.FindAll()
    foreach ($pt in $profileTemplates) 
       ### Apply permissions for the ClmUsers and ClmManagers Groups
       Write-Host "Setting ACLs for the Profile Template: " -ForegroundColor Green -NoNewLine
    $p = [ADSI]$pt.path $p.Name .\set-ClmProfileTemplateAdAcl.ps1 $pt.Path $clmUsersSid .\set-ClmProfileTemplateAdAcl.ps1 $pt.Path $clmManagersSid }

    Set-ClmProfileTemplateAdAcl.ps1 Script:

    param($dn, $sid)
    Function Set-ProfileTemplateACL 
    { if ($sid -eq $null) { Write-Host "Sid cannot be null: " -fore red return 1 } if ($dn -eq $null) { Write-Host "Dn cannot be null: " -fore red return 1 } $pt = [ADSI]$dn if ($pt.distinguishedName -eq $null) { Write-Host "Object not found: " $dn -fore red return 1 } $enrollAce = '(OA;;CR;f79c4213-4d44-4255-9440-8795bfa20281;;' + $sid + ')' $readAce = '(A;;LCRPRC;;;' + $sid + ')' $currentACL = $pt.psbase.ObjectSecurity.GetSecurityDescriptorSddlForm(
    [System.Security.AccessControl.AccessControlSections]::All) $pt.psbase.ObjectSecurity.SetSecurityDescriptorSddlForm($currentACL + $enrollAce + $readAce) $pt.psbase.commitchanges() Write-host "Permission applied." -fore green }

    Applying CM Profile Template Policy Permissions

    Profile Template permissions are also stored inside the directory objects themselves, stored as SDDL strings inside the Profile Template XML strings.
    Those permissions can be viewed using the CM portal when browsing the Policies contained in each Profile Template.
    Modifying the Profile Template XMLs is not supported (and I don’t suggest messing with them unless you are just tooling around with a development box).
    The scripts in this article do not apply those permissions.
    In the future I hope to add a utility for applying the Profile Template Policy Permissions.

    Additional Information

    This article dives pretty deep into scripting for CM without regard for the uninitiated. The following resources go into much greater detail for the whole product:

    Also, the TechNet Forum is a great place to post questions regarding CLM.
    It is frequented by Microsoft people involved with CLM, as well as MVPs with deep deployment knowledge.

    About the Author

    Craig Martin speaks in the third person when writing his own brief biography.
    He has been sync’ing for longer than he ever thought possible.
    He has also done work integrating ILM and CLM, spending countless hours weeding out issues in his lab environments learning CLM lessons the hard way in order to beat his chest in triumph and share his scars as lessons in a self deprecating manner.


    CraigMartin – Oxford Computer Group – http://identitytrench.com
    • Edited by Craig Martin Thursday, July 9, 2009 9:16 PM formatting really sucked
    Friday, June 26, 2009 9:11 PM