Windows "Manage Private Keys Permissions" Missing

I had a problem remote desktoping to a server today - “internal error”. I eventually tracked it down to the fact that the Letsencrypt certificate we use for Remote Desktop didn’t have read permissions set for the NETWORK SERVICE to read the private key.

I’ve used this for setup for a while and wasn’t sure why I had an issue now. I looked at some of the older certificates and they did have this permission, it seems only the most recent two don’t. The certificates were all generated with Certify The Web, so what is it in Certify The Web that controls this permission? Why was it granted in the past and not now?

I’ve been trying to think of anything I may have changed. The only thing I am aware of is that I used to use my own powershell script to update the RDP certificate thumbprint in the registry and then discovered there is a built in Task to do that so switched to using that. My own script only updated the registry, it didn’t touch the certificate access permissions.

HI Jason,

What version of Certify The Web are you using? A recent beta version (5.9.x) did have an issue related to this.

If you are just using the current release version, have you ever used the background service as a different user other than the default (Local System)? Some users try to do this (change the service account for the background service) but it’s unsupported due to the complexity of permissions (and key stores etc) involved.

We do not currently set any special permission attributes on the certificates, we simply store them as Local System with X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable flags set.

Where are you checking the permissions for NETWORK SERVICE?

Do you have any other deployment tasks enabled?

I see the microsoft docs do suggest that you need to enable read for the certificate keys: Remote Desktop listener certificate configurations - Windows Server | Microsoft Learn - as a workaround you could run your own script to do this but our basic built in deployment task does not do this. From memory this is the first time this issue has come up.

I used procmon to capture and confirm the problem when I tried to remote desktop in:

Time of Day	Process Name	PID		Operation	Path																													Result			Detail
31:43.3		lsass.exe		1400	CreateFile	C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\6f021f9dfe8e073c19841e9f6e5be9ba_ed053d70-8e6a-4081-8aa8-31b1245f1c3b	ACCESS DENIED	Desired Access: Generic Read, Disposition: Open, Options: Sequential Access, Synchronous IO Non-Alert, Non-Directory File, Attributes: n/a, ShareMode: Read, AllocationSize: n/a, Impersonating: NT AUTHORITY\NETWORK SERVICE

There is a way to edit the permissions using icacls but I found it much easier to do it using the MMC certificate plugin (as detailed in the article you linked to).

If I looked at a non-working certificate the only permissions were for system (full) and administrators (read).
(I’m a new user on this forum so it won’t let me post more than one picture)

To ‘fix’ the problem I just had to add read access to NETWORK SERVICE
(I’m a new user on this forum so it won’t let me post more than one picture)

As I said, the only thing I had changed was moving from my script to ‘Deploy to RDP Listener Service’ instead. But my script doesn’t do anything with permissions, it just writes the thumbprint to the appropriate registry key!

# this grabs the certificate thumbprint and tells RDP to use it
$HOSTNAME="mycomputer.mydomain.com"
$THUMBPRINT = (ls Cert:\LocalMachine\my | WHERE {$_.Subject -match $HOSTNAME } | Sort-Object -Property NotAfter -Descending  | Select-Object -First 1).Thumbprint

if (!$THUMBPRINT) { 
    write-host "Failed to find certificate thumbprint for ${HOSTNAME}, quitting."
} else {
    write-host "Found certificate thumbprint for ${HOSTNAME}: ${THUMBPRINT}"
    write-host "Configuring RDP"
    & wmic /namespace:\\root\CIMV2\TerminalServices PATH Win32_TSGeneralSetting Set SSLCertificateSHA1Hash="$THUMBPRINT" 
    write-host `Done!`
}

Clearly I can’t explain why running my script as a powershell task adds a permission that your ‘Deploy to RDP Listener Service’ doesn’t. I double checked that it was running my script as a task that made the change by getting a new certificate and checking its permissions before and after my script ran.

I’m using the current release version, 5.6.8.0. I haven’t ever used the background service as a different user other than the default (Local System). I don’t have any other deployment tasks enabled.

Edit: BTW, I realise I could have passed the Thumbprint from Certify The Web as a variable but I haven’t tried using Certify The Web variables and I already had the script to reuse.

Follow up picture!

If I looked at a non-working certificate the only permissions were for system (full) and administrators (read).
Non-working

Follow up picture!

To ‘fix’ the problem I just had to add read access to NETWORK SERVICE
Working

This is not an issue for me, but my certificates have the same default permissions. I don’t keep very old certificates around, so I cannot say if this behavior is recent or long-standing.

There’s a fairly in-depth example script posted here: Post Request Script examples - #4 by andresr

This is beyond the simple scripts we have built-in (which are here: certify-plugins/src/DeploymentTasks/Core/Providers/Assets at development · webprofusion/certify-plugins · GitHub) but I note that it has a script block to set the ACL, in this case it takes the last key written and sets the permission on that:

                        $FilePath = "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys"
                        $File = Get-ChildItem $FilePath | Sort-Object LastWriteTime -Descending | Select-Object -First 1
                        # Specify account
                        $Account = "NT AUTHORITY\NETWORK SERVICE"
                        # Get current ACL on the private key
                        $ACL = Get-Acl -Path $File.FullName
                        # Set new rule
                        $rule = New-Object System.Security.AccessControl.FileSystemAccessRule("$Account", "Read", "Allow")
                        # Add rule to the ACL
                        $ACL.AddAccessRule($rule)
                        # Set new ACL to the private key
                        Set-Acl -Path $File.FullName -AclObject $ACL

Thanks for that @webprofusion

It’s useful to have that in case something changes and I need to set the permission as part of my own script. What remains a mystery is why the permissions change when I run my existing script, I can only assume that it is something that powershell does itself in the background but I can’t think of any reason why it would do that.

For what it’s worth I tried allowing Network Service read at the MachineKeys level to see if new certs might get the required permission without a script and I broke every certificate on my machine :slight_smile: