On a Windows Server 2012 R2 machine attached to a domain, I am running the following statements:
$target_machine_fqdn = [System.Net.Dns]::GetHostByName($env:computerName)
$certificate_request = Get-Certificate `
-Template 'AcmeComputer' `
-DnsName $target_machine_fqdn `
-CertStoreLocation 'Cert:\LocalMachine\My'
I'm requesting a certificate for the host from the domain's CA. The statement returns with no error. A certificate is generated for the machine and placed in it's "Cert:\LocalMachine\My" as requested.
Problem: I can't figure out how to grant a service account rights to the certificate's private key.
Now, there are about 1,000 articles instructing people how to grant permission by retrieving the UniqueKeyContainerName
with code that starts like the following:
$certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
That won't work here. While the certificate has a private key, the private key data member is null:
In the cases where the solution I just eluded to works, the private key is on the file system. However, in this case, the private key is in the registry at the following:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SystemCertificates\MY\Keys
When using the Certificates MMC-snapin, I can see the certificate. I can manage permissions on the private key. So, I know it's there. Unfortunately, I need to automate the permission assignment so using the Certificates MMC-snapin isn't an option. I need to do this through Powershell some how.
I recently went through automating access to certificate private key myself. I too found a number of places telling me to modify the ACLs of the key data on the hard drive, but that was not satisfying since when I checked the permissions to the private key using PowerShell the user I added wasn't listed. So much web searching, a few articles, and a fair bit of trial and error led me to this.
I start by defining the user object, and the access that I want to grant them:
# Create NTAccount object to represent the account
$AccountName = 'Domain\UserName'
$User = New-Object System.Security.Principal.NTAccount($AccountName)
# Define AccessRule to be added to the private key, could use 'GenericRead' if all you need is read access
$AccessRule = New-Object System.Security.AccessControl.CryptoKeyAccessRule($User, 'FullControl', 'Allow')
Then I open the local machine certificate store as Read/Write, and find the certificate that I'm looking for:
# Define the thumbprint of the certificate we are interested in
$Thumb = '63CFDDE9A748345CD77C106DAA09B805B33951BF'
# Open Certificate store as read/write
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine")
$store.Open("ReadWrite")
# Look up the certificate's reference object in the store
$RWcert = $store.Certificates | where {$_.Thumbprint -eq $Thumb}
Then I make a new CSP (Crypto Service Provider) parameter set, based off the existing certificate, add the new access rule to the parameter set.
# Create new CSP parameter object based on existing certificate provider and key name
$csp = New-Object System.Security.Cryptography.CspParameters($RWcert.PrivateKey.CspKeyContainerInfo.ProviderType, $RWcert.PrivateKey.CspKeyContainerInfo.ProviderName, $RWcert.PrivateKey.CspKeyContainerInfo.KeyContainerName)
# Set flags and key security based on existing cert
$csp.Flags = "UseExistingKey","UseMachineKeyStore"
$csp.CryptoKeySecurity = $RWcert.PrivateKey.CspKeyContainerInfo.CryptoKeySecurity
$csp.KeyNumber = $RWcert.PrivateKey.CspKeyContainerInfo.KeyNumber
# Add access rule to CSP object
$csp.CryptoKeySecurity.AddAccessRule($AccessRule)
Then we instantiate a new CSP, with those parameters, which will apply the new access rule to the existing certificate based off the flags we defined, and the key info we gave it.
# Create new CryptoServiceProvider object which updates Key with CSP information created/modified above
$rsa2 = New-Object System.Security.Cryptography.RSACryptoServiceProvider($csp)
Then we just close the certificate store, and we're all done.
# Close certificate store
$store.Close()
Edit: After looking around I realized that I had a couple certs that are the same. I believe that this is due to a non-RSA cipher being used to encrypt the private key. I used some of the info from this answer that explains how to work with a third party CNG crypto provider. I didn't like having to download an assembly to do the things in that answer, but I used a little bit of the code to get the path to the key (yes, there is a key on the drive), and added an ACL to the file, which did work for delegating rights to the private key. So here's what I did...
First, we verify that the certificate has a CNG based key:
[Security.Cryptography.X509Certificates.X509CertificateExtensionMethods]::HasCngKey($Certificate)
If that returns True
then we're a go to move on past that. Mine did, I'm guessing yours will too. Then we find that key on the hard drive by reading the PrivateKey data (which is missing from $Certificate
), and getting the UniqueName
for it, and then searching the Crypto folder for that file.
$privateKey = [Security.Cryptography.X509Certificates.X509Certificate2ExtensionMethods]::GetCngPrivateKey($Certificate)
$keyContainerName = $privateKey.UniqueName
$keyMaterialFile = gci $env:ALLUSERSPROFILE\Microsoft\Crypto\*Keys\$keyContainerName
Then I grabbed the current ACLs for the file, made up a new AccessRule to give the desired user access to the file, added the rule to the ACLs I just grabbed, and applied the updated ACL back to the file.
$ACL = Get-Acl $keyMaterialFile
$AccountName = 'Domain\User'
$User = New-Object System.Security.Principal.NTAccount($AccountName)
$AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($User,'FullControl','None','None','Allow')
$ACL.AddAccessRule($AccessRule)
Set-Acl -Path $keyMaterialFile -AclObject $ACL
After that I was able to look in certlm.msc and verify that the user had rights to the private key.
Dependency Update: Looks like Microsoft now publishes Security.Cryptography.dll, so you don't have to compile it off GitHub. If you have the AzureRM module installed you can find the DLL in a number of the component modules (I grabbed it from AzureRM.SiteRecovery).
The TheMadTechnician answered this question like a mf'n champ. Should he ever need me to help him bury a body, he need only call. I am adding to his answer with details for folks that cannot build/deploy the assembly
NOTE: The clrsecurity project had been posted to CodePlex, which was shutdown in 2017. The project was moved to github where it can be downloaded. The clrsecurity assembly referenced in the post is no longer supported.
Also, credit to Vadims Podāns (Crypt32) who wrote the article Retrieve CNG key container name and unique name, which helps readers access the CNG private key using unmanaged code in Powershell.
If you are like me and cannot use the clrsecurity assembly, the .NET 4.5.6 framework introduced a elements we can leverage. Consider the following:
## Identify the certificate who's private key you want to grant
## permission to
$certificate = $(ls 'cert:\LocalMachine\My\C51280CE3AD1FEA848308B764DDCFA7F43D4AB1A')
## Identify the user you'll be granting permission to
$grantee_name = 'foo\lordAdam'
$grantee = New-Object System.Security.Principal.NTAccount($grantee_name)
## Get the location and permission-of the cert's private key
$privatekey_rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate)
$privatekey_file_name = $privatekey_rsa.key.UniqueName
$privatekey_path = "${env:ALLUSERSPROFILE}\Microsoft\Crypto\Keys\${privatekey_file_name}"
$privatekey_file_permissions = Get-Acl -Path $privatekey_path
## Grant the user 'read' access to the key
$access_rule = New-Object System.Security.AccessControl.FileSystemAccessRule($grantee, 'Read', 'None', 'None', 'Allow')
$privatekey_file_permissions.AddAccessRule($access_rule)
Set-Acl -Path $privatekey_path -AclObject $privatekey_file_permissions
We're using the GetRSAPrivateKey() method of the System.Security.Cryptography.X509Certificates.RSACertificateExtensions
static class to return us the private key. We then grab the UniqueName (Key property has a UniqueName property). We use that to deduce the key file's location. The rest is granting file permissions.
If you want to see where private keys are stored, check out Key Storage and Retrieval.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With