Remove-DbaComputerCertificate - Add -DeleteKey to delete the private key with the certificate - #10724
Conversation
…key with the certificate Removing a certificate leaves its private key file on disk, under RSA\MachineKeys for a legacy CSP key and under Crypto\Keys for a Key Storage Provider key. With -DeleteKey the key is deleted as well: a legacy CSP key through its own provider (deleting it through the CNG bridge leaves the file behind), a CNG key through CngKey.Delete. The key stays, and the output says so, when another certificate in the same store location still uses it. The output gains a PrivateKey property. The integration tests no longer skip on PowerShell 7; the reason (Add-DbaComputerCertificate) was fixed long ago. (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
potatoqualitee
left a comment
There was a problem hiding this comment.
Blocking: public/Remove-DbaComputerCertificate.ps1 enumerates only the eight StoreName enum values when checking whether another certificate shares the key. That excludes LocalMachine\WebHosting and custom stores. Reproduce by copying the certificate from LocalMachine\My into LocalMachine\WebHosting, then removing the My copy with -DeleteKey: the scan misses the remaining WebHosting certificate, deletes their shared private key, and leaves the installed WebHosting certificate unusable for TLS. This contradicts the documented key-preservation guarantee and can break an IIS site. Please enumerate the stores actually present under the selected location, open them by string name, and add a regression covering WebHosting (or another non-enum store).
…ion before deleting a shared key The shared-key scan enumerated only the eight StoreName values, so a copy of the certificate in WebHosting or a custom folder was not seen and the key it still used was deleted. The folders now come from the Cert: drive and are opened by name, the key is kept when the folders cannot be listed, and -Folder accepts any folder name for the removal itself. (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Fixed in 4cb5f6e.
Verified: on the previous commit the new test fails with created by Claude and reviewed by Andreas Jordan |
potatoqualitee
left a comment
There was a problem hiding this comment.
Blocking safety defect: the previous WebHosting/custom-store issue is fixed, but an unreadable individual store can still cause deletion of a key used by a remaining certificate. In public/Remove-DbaComputerCertificate.ps1:253-275, an exception from X509Store.Open or enumerating Certificates is swallowed at lines 269-271 and treated as though that store had no shared certificate. Concrete path: copy a certificate/key reference from LocalMachine\My into a custom store, allow the caller to list the store names and remove from My but deny read access to the custom store, then run Remove-DbaComputerCertificate -DeleteKey on the My copy. The scan suppresses the access failure, $sharedWith stays empty, and line 299 deletes the key, leaving the custom-store certificate unusable for TLS/signing. X509Store.Open explicitly requires enumeration permission and can throw SecurityException/CryptographicException; listing a registry-backed store does not prove it can be opened. Please treat any individual store scan failure as an incomplete scan: clear the deletion candidate and return a Not deleted result, as the top-level listing-failure path already does. Add a real-store ACL regression that restores the ACL in finally.
…re location cannot be read
A folder that is listed under Cert:\<Store> can still refuse to open or to enumerate its
certificates, for example when the caller has no read access to its registry key. The shared-key
scan treated such a folder as empty and deleted the key, although a certificate in that folder may
still use it. Now every folder that cannot be read makes the scan incomplete: the key stays and the
output names the folder ("Not deleted: Cert:\LocalMachine\<folder> could not be read, ...").
The regression test copies the certificate into a custom folder, takes read access away from the
current user with a deny rule on the folder's registry key (set and removed through the .NET
registry API, because Set-Acl needs the read rights the rule denies), expects the key to survive
the removal from My, removes the rule in finally and then deletes the key with the last copy.
(do Remove-DbaComputerCertificate)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks, agreed: a listed folder that cannot be opened is an incomplete scan, not an empty folder. Fixed in a9a91af. Command: every folder whose Regression test (real store, ACL restored in finally): the certificate is copied into a custom folder Verification: on the previous commit the new test fails with created by Claude and reviewed by Andreas Jordan |
potatoqualitee
left a comment
There was a problem hiding this comment.
Blocking safety defect: the shared-key scan only checks the selected certificate StoreLocation. A certificate copied from LocalMachine\My into CurrentUser\My with X509Store.Add retains the same machine-key provider reference, but CurrentUser\My does not include LocalMachine\My. Removing the CurrentUser copy with -Store CurrentUser -DeleteKey therefore skips the target copy, never sees the retained LocalMachine certificate, and deletes their machine key, breaking SQL Server/IIS TLS or signing that still relies on the machine certificate.
Please scan both relevant store locations before deleting a persisted key, keep the key whenever either required scan is incomplete, and add a real-store regression that copies a machine-key certificate across LocalMachine/CurrentUser and removes one copy with -DeleteKey.
|
@potatoqualitee As we have some back and forth between our LLMs - should we try to get this fixed or just close this pull reqeust and let the user remove the key? About "CurrentUser"- there might be a lot of users on that machine and I think we can only access the one we use to connect. So there will always be a risk with "-DeleteKey". I will start my lab later and ask Claude to respond, but if you are unsure, just close this PR. |
|
im happy to see it through |
…eting a key A copy of a certificate keeps its key reference across store locations, so a machine certificate copied into CurrentUser\My points at the same machine key. The shared-key scan looked only at the location named by -Store and deleted the key under the other copy. Now every folder of LocalMachine and of the CurrentUser store of the calling account is scanned whatever -Store says, and the key stays when either folder list or any folder cannot be read. The help says which stores are checked and that other accounts' personal stores are out of reach. (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…icate is removed Some CurrentUser folders (Root, CA, TrustedPeople, ...) also show the certificates of the LocalMachine folder of the same name. With the scan running before the removal, the certificate being removed from LocalMachine\TrustedPeople showed up in Cert:\CurrentUser\TrustedPeople as a sharer of its own key and the key stayed. The scan now runs after the removal: the mirror image is gone with the certificate, a real copy in the CurrentUser store stays. A LocalMachine sharer that is mirrored is reported once. (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Agreed, and reproduced before changing anything: a certificate from Command: both store locations are scanned whatever The help now says exactly what is checked, and what is not: the personal stores of other accounts on the computer cannot be checked, because they live in those accounts' registry hives and no API opens them by account, so a key that only a certificate in one of them still uses is deleted. That limit is inherent to any implementation; for comparison, the A Windows fact that shaped the fix: several Regression test (real stores): two machine-key certificates are copied into Verification: on a9a91af the new test fails with created by Claude and reviewed by Andreas Jordan |
…key instead of opening every key The scan for another certificate that uses the key opened the private key of every certificate in every folder. On Windows PowerShell a smart card certificate in the store of the user then prompts for the card, and an unattended run hangs on that prompt. Certificates that share a private key share the public key, so the public keys are compared and no other private key is opened. The one case that misses is a legacy CSP container that holds a key exchange key and a signature key, because such a container can only be deleted as a whole. Before a legacy container is deleted, its other key is opened through the provider of the certificate and, if a certificate still uses it, the container stays: "Kept, key container shared with <thumbprint> in <store>". (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…provider that holds both key types (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ates without Exportable, which certreq refuses with an existing key set (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
One more change on top, from a lab finding today rather than from a review: 7f1acce, 50edef4, f7ad6b9. The finding: the scan opened the private key of every certificate in every folder of both store locations to compare the key file names. When an RDP session with a smart card propagated a certificate with the Microsoft Smart Card Key Storage Provider into The change: certificates that share a private key share the public key, so the scan compares public keys and never opens another certificate's key. That also removes the last silent skip (a certificate whose key could not be opened counted as not sharing). The one case a public key comparison misses is a legacy CSP container that holds a key exchange key and a signature key, because such a container can only be deleted as a whole. So before a legacy container is deleted, its other key is opened through the provider of the certificate being removed (never a foreign provider, so no prompt), and if a certificate in either store location still uses it, the container stays: Test: two self-signed certificates on one container of the Enhanced RSA and AES provider (the SChannel provider creates no signature keys), one on the key exchange key and one on the signature key, built with certreq on the existing key set. Removing the first with Verification: 13/13 in the lab on PowerShell 7.6 and Windows PowerShell 5.1 with the smart card certificate present in created by Claude and reviewed by Andreas Jordan |
|
I’d request changes. I reviewed head 1. [P1] The shared-key scan misses archived certificatesLocation: The scan opens each store with: $otherStore.Open(
[System.Security.Cryptography.X509Certificates.OpenFlags]"ReadOnly, OpenExistingOnly"
)That excludes archived certificates. Windows skips those certificates during normal enumeration, and .NET provides Failure scenario: Extend the existing shared-certificate test by marking the remaining copy in Fix: Include archived certificates in every store opened for the safety scan: $otherStore.Open(
[System.Security.Cryptography.X509Certificates.OpenFlags]"ReadOnly, OpenExistingOnly, IncludeArchived"
)Add a regression asserting that an archived sharer preserves the key, reports 2. [P2] Matching public keys does not prove the certificates share a key containerLocation: The new comparison treats matching public-key bytes as proof that deleting the target’s key would affect the other certificate. That is conservative, but the same key material can exist in separate persisted containers. For example, importing the same PFX independently into machine and user keysets creates separate storage rather than the cross-store reference used in the current tests. Windows explicitly supports selecting either keyset during PFX import. ([Microsoft Learn]4) For that arrangement, the code’s cleanup sequence is:
Fix: Preserve the no-smart-card-prompt behavior, but distinguish actual container references using certificate metadata. Verification and merge recommendationThe earlier WebHosting/custom-store, unreadable-store, and cross-location issues are addressed in this revision and have regression coverage. The latest The archived-certificate issue is the merge blocker. The independent-container issue is a cleanup defect, not a destructive one. I checked the source and Microsoft’s API documentation, but could not execute the Windows certificate-store tests in this Linux environment. I have not submitted a GitHub review or changed the PR. |
…ize a shared key by its container reference The scan for certificates that still use the key opened every folder without IncludeArchived, so an archived copy was invisible and its key was deleted. Now every scanned folder includes archived certificates, and the certificate to remove is found by its thumbprint even when it is archived, so the copy the output names can be removed as well. A shared key was recognized by the public key. The same key can sit in a container of its own, for example the same PFX file imported once for the machine and once for the user, and then the kept container was orphaned once the other certificate went. Now the key reference stored with each certificate (provider, container name, machine or user key set) is read through CertGetCertificateContextProperty, which does not open the key, and only a certificate that names the same container keeps it. Every legacy Microsoft RSA provider opens the same containers, so the provider name only counts for a Key Storage Provider; a machine certificate may name a legacy container by its unique name while the key reports its friendly name, so both names count. A certificate that uses the other key of a legacy container is found the same way, which replaces opening that key. (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Both points addressed in d4da66e. 1. Archived certificates: every folder the scan opens now includes archived certificates ( 2. Container identity instead of the public key: the scan now reads
A certificate with the same public key in another container no longer keeps the key. A certificate whose key reference cannot be read keeps the key when the public key matches, as the conservative fallback. The same reference also finds a certificate that uses the other key of a legacy container, so the other key of the container is no longer opened either. Test: a self-signed certificate exported to PFX and imported into Verification: 15/15 in the lab on PowerShell 7.6 and Windows PowerShell 5.1, stores and key files clean afterwards. CI is running. created by Claude and reviewed by Andreas Jordan |
…der ProgramData for a service account The test for a key held in a separate user container looked for the user key file in the roaming profile only. The CI runner is a LocalSystem service, and Windows keeps the user keys of LocalSystem and the other service accounts under ProgramData\Microsoft\Crypto\RSA\<SID>, so the file was not found there and the test failed. The fixture now looks in both places. (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
One follow-up on the test, 78b3e70: on d4da66e the created by Claude and reviewed by Andreas Jordan |
|
so close! I’d still request changes. Reviewed head P1: A legacy key referenced through CNG metadata can be deleted while another certificate still uses itLocation: The shared-container check rejects any remaining certificate with provider type } elseif ($legacyProvider) {
$sameContainer = $otherKeyInfo.ProviderType -ne 0
}That rejects the certificate even when its container name, machine/user key set, and provider name match the key being deleted. The public-key match does not rescue it, because that fallback applies only when the provider-info object is missing. Why this is a valid sharing scenario: .NET’s Regression scenario to add:
Tracing the current code, the remaining certificate’s provider type is Required fix: Recognize CNG-bridge references to the same legacy provider/container instead of treating every zero provider type as unrelated. Preserve the machine/user key-set check and the distinction between genuinely separate containers. The regression should assert that the key file survives, the remaining certificate can reopen and use its key, and removing the final certificate deletes it. Rest of the reviewThe earlier archived-certificate and independent-container findings are fixed, with regression tests. Coverage also includes WebHosting, unreadable stores, cross-location sharing, and both keys in a legacy container. Those earlier findings should not remain blockers. The current commit’s CI workflows, including |
…renced through the CNG bridge A certificate that got the key of another certificate through CopyWithPrivateKey with an RSACng stores a key reference with the provider name and container name of the legacy key, but with a provider type of 0, the value of a Key Storage Provider reference. The scan treated every provider type of 0 as a different container when the key being deleted was a legacy key, so the copy lost its key. A provider type of 0 now only rules a certificate out when it names a Key Storage Provider; a legacy provider name is the same container. (do Remove-DbaComputerCertificate) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Confirmed and fixed in e8900b2. I built the scenario in the lab on both editions (pwsh 7.6 and Windows PowerShell 5.1): Fix: when the key being deleted is a legacy key, a provider type of 0 now only rules a certificate out when its provider name is a Key Storage Provider. That is checked against Regression test: the CNG bridge copy in created by Claude and reviewed by Andreas Jordan |
|
Passed! 🥳 |
Why
Remove-DbaComputerCertificateremoves the store entry and leaves the private key file on disk: underRSA\MachineKeysfor a legacy CSP key, underCrypto\Keysfor a Key Storage Provider key. That is what the certificate console does too, but it means every certificate the dbatools tests create leaves a key file behind (31 orphans on one lab host after one day of certificate test runs), and a DBA cleaning up certificates has no way to take the keys with them short of a file system dig.What changes
A
-DeleteKeyswitch, named after the switch of the same purpose on theCert:drive in Windows PowerShell, which PowerShell 7 does not have. Off by default, so nothing changes for existing callers.With the switch:
GetRSAPrivateKey/GetECDsaPrivateKey), which works for legacy CSP keys and Key Storage Provider keys in both PowerShell editions.CspParameterswithUseExistingKey,PersistKeyInCsp = $false), because deleting it through the CNG bridge removes the container reference but leaves the key file behind, which I saw in the lab. A CNG key is deleted withCngKey.Delete().The output gains a
PrivateKeyproperty:Kept,Deleted,Kept, shared with <thumbprint> in <store>,Not deleted: <reason>,Nonefor a certificate without a private key,$nullwhen the certificate was not found.Design question for the review: opt-in is the conservative choice and mirrors the
Cert:drive. An always-on deletion would change what the command has done since 2018.Tests
The integration
Describeno longer skips on PowerShell 7; the reason it did (Add-DbaComputerCertificateon that edition) has been fixed for a long time, and the file runs green there now.New context with four tests, all local: a legacy CSP key stays without the switch; a legacy CSP key (from
New-DbaComputerCertificate) and a Key Storage Provider key (fromNew-SelfSignedCertificate) are deleted with the switch and their files are gone; a certificate copied intoTrustedPeoplekeeps its key when removed fromMywith the switch, and loses it when the copy is removed too. The parameter list is updated.Verification
Lab (ADMIN01, local, tests through the harness):
Remove-DbaComputerCertificate.Tests.ps19/9 green.PrivateKeyproperty on the output), the four existing tests pass.CngKey.Delete()on a legacy CSP key opened through the CNG bridge leaves the file underRSA\MachineKeysbehind;RSACryptoServiceProviderwithPersistKeyInCsp = $falseremoves it.created by Claude and reviewed by Andreas Jordan
🤖 Generated with Claude Code