Skip to content

Remove-DbaComputerCertificate - Add -DeleteKey to delete the private key with the certificate - #10724

Merged
potatoqualitee merged 11 commits into
developmentfrom
feature-remove-dbacomputercertificate-deletekey
Sep 23, 2026
Merged

potatoqualitee merged 11 commits into
developmentfrom
feature-remove-dbacomputercertificate-deletekey

Conversation

@andreasjordan

Copy link
Copy Markdown
Collaborator

Why

Remove-DbaComputerCertificate removes the store entry and leaves the private key file on disk: under RSA\MachineKeys for a legacy CSP key, under Crypto\Keys for 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 -DeleteKey switch, named after the switch of the same purpose on the Cert: drive in Windows PowerShell, which PowerShell 7 does not have. Off by default, so nothing changes for existing callers.

With the switch:

  • The key is opened through the CNG API (GetRSAPrivateKey / GetECDsaPrivateKey), which works for legacy CSP keys and Key Storage Provider keys in both PowerShell editions.
  • Before anything is deleted, every store of the same location is searched for another certificate that uses the same key container (a copy of the certificate in another folder, or a renewed certificate that reused the key). If one exists the key stays, and the output names it.
  • A legacy CSP key is deleted through its own provider (CspParameters with UseExistingKey, 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 with CngKey.Delete().
  • For a machine key the command checks afterwards that the key file is gone and reports if it is not.

The output gains a PrivateKey property: Kept, Deleted, Kept, shared with <thumbprint> in <store>, Not deleted: <reason>, None for a certificate without a private key, $null when 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 Describe no longer skips on PowerShell 7; the reason it did (Add-DbaComputerCertificate on 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 (from New-SelfSignedCertificate) are deleted with the switch and their files are gone; a certificate copied into TrustedPeople keeps its key when removed from My with the switch, and loses it when the copy is removed too. The parameter list is updated.

Verification

Lab (ADMIN01, local, tests through the harness):

  • Windows PowerShell 5.1: Remove-DbaComputerCertificate.Tests.ps1 9/9 green.
  • PowerShell 7.6.3: 9/9 green, the first time this file's integration tests ran on that edition.
  • Red on old (v2.9.0 code with the new test file, 5.1): the parameter test and the four new tests fail (unknown parameter, and no PrivateKey property on the output), the four existing tests pass.
  • The lab fact behind the CSP branch: CngKey.Delete() on a legacy CSP key opened through the CNG bridge leaves the file under RSA\MachineKeys behind; RSACryptoServiceProvider with PersistKeyInCsp = $false removes it.

created by Claude and reviewed by Andreas Jordan

🤖 Generated with Claude Code

…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 potatoqualitee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

Fixed in 4cb5f6e.

  • The shared-key scan now takes the folders from Get-ChildItem Cert:\<Store> and opens each one by name with ReadOnly, OpenExistingOnly, so WebHosting and custom folders are checked as well. On this machine that is 18 folders instead of the 8 enum values.
  • If the folder list cannot be read, the key is not deleted and the output says so (Not deleted: the folders of Cert:\LocalMachine could not be listed, ...), rather than falling back to a partial scan.
  • The inline Get-CoreCertStore/Get-CoreCertificate open the target folder by name too, so -Folder WebHosting removes from that folder. Their old ValidateSet had "AuthRoot, CertificateAuthority" as a single entry, so neither of those worked either.
  • Regression test: a copy of the certificate in LocalMachine\WebHosting (the test creates the folder when the computer has no IIS and removes its registry key again afterwards). Removing the My entry with -DeleteKey reports Kept, shared with <thumbprint> in Cert:\LocalMachine\WebHosting and the key file stays; removing the WebHosting entry deletes the key.

Verified: on the previous commit the new test fails with PrivateKey = Deleted, which is exactly the scenario you described. Lab 10/10 in Windows PowerShell 5.1 and pwsh 7, CI green with the file running on the test (default) lane.

created by Claude and reviewed by Andreas Jordan

@potatoqualitee potatoqualitee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

Thanks, agreed: a listed folder that cannot be opened is an incomplete scan, not an empty folder. Fixed in a9a91af.

Command: every folder whose Open or certificate enumeration throws is now recorded, and after the scan the key stays with Not deleted: Cert:\LocalMachine\<folder> could not be read, so it is unknown whether another certificate uses the key (all unreadable folders are named). The order after the scan is: folder list unreadable -> not deleted; a certificate that provably shares the key -> Kept, shared with ...; any unreadable folder -> not deleted. A proven sharer keeps the key either way, so its more precise message wins. The -DeleteKey help got a sentence for the new outcome.

Regression test (real store, ACL restored in finally): the certificate is copied into a custom folder dbatoolsci_unreadable, then a deny rule for the current user (QueryValues, EnumerateSubKeys) goes onto the folder's registry key inside try. In that state the folder is still listed under Cert:\LocalMachine, Get-ChildItem on it throws, and removing the My copy with -DeleteKey must return the new reason and leave the key file in place. The finally removes the rule (AfterAll removes it once more, in case the test never reached the finally), and then the copy is removed from the custom folder with -DeleteKey, which deletes the key. One trap worth knowing: Set-Acl cannot undo that rule, because it opens the key with the read rights the rule denies. The test therefore sets and removes the rule through RegistryKey.OpenSubKey with ReadPermissions, ChangePermissions and RemoveAccessRule on the exact same rule.

Verification: on the previous commit the new test fails with Deleted (the key was gone). With the fix the file is 11/11 in the lab on PowerShell 7.6 and Windows PowerShell 5.1, the machine's stores are clean afterwards, and CI is green on all lanes with the file passing on the default lane.

created by Claude and reviewed by Andreas Jordan

@potatoqualitee potatoqualitee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@andreasjordan

Copy link
Copy Markdown
Collaborator Author

@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.

@potatoqualitee

Copy link
Copy Markdown
Member

im happy to see it through

andreasjordan and others added 2 commits September 22, 2026 16:38
…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>
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

Agreed, and reproduced before changing anything: a certificate from New-DbaComputerCertificate copied into CurrentUser\My with X509Store.Add reports IsMachineKey with the same UniqueName, and on a9a91af removing that copy with -Store CurrentUser -DeleteKey returned PrivateKey = Deleted; the LocalMachine\My certificate that stayed behind then failed with "Keyset does not exist". Fixed in aec854a and 4ca55ca.

Command: both store locations are scanned whatever -Store says: every folder of LocalMachine and every folder of the CurrentUser store of the account running the command. The key stays when either folder list cannot be read (Not deleted: the folders of Cert:\... could not be listed, ...) or when any folder in either location cannot be read (Not deleted: Cert:\<Location>\<Folder> could not be read, ...). A sharer is reported with its location, Kept, shared with <thumbprint> in Cert:\CurrentUser\My.

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 -DeleteKey of the Cert: drive in Windows PowerShell deletes the key without looking anywhere at all.

A Windows fact that shaped the fix: several CurrentUser folders (Root, CA, TrustedPeople, ...) are composite views that also show the certificates of the LocalMachine folder of the same name (physical store .LocalMachine); My is not one of them. 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 therefore runs after the certificate is removed from its folder: the mirror image is gone with it, a real copy in the user store stays, and no special-casing by folder name is needed. A LocalMachine sharer that is mirrored into the CurrentUser folder of the same name is reported once. The key object itself is still read while the certificate is in the store, as before.

Regression test (real stores): two machine-key certificates are copied into CurrentUser\My. The first is removed from CurrentUser first: Kept, shared with <thumbprint> in Cert:\LocalMachine\My, the key file stays and the LocalMachine certificate's key still opens; then removed from LocalMachine: Deleted, file gone. The second goes the other way round: removed from LocalMachine first, Kept, shared with <thumbprint> in Cert:\CurrentUser\My, then from CurrentUser, Deleted.

Verification: on a9a91af the new test fails with Deleted for the first removal. With the fix the file is 12/12 in the lab on PowerShell 7.6 and Windows PowerShell 5.1, and the machine's stores and key folders are clean afterwards. CI is running.

created by Claude and reviewed by Andreas Jordan

andreasjordan and others added 3 commits September 22, 2026 17:54
…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>
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

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 CurrentUser\My on the lab workstation, every -DeleteKey call then popped the "Windows Security: connect a smart card" dialog, in Windows PowerShell 5.1 and in PowerShell 7 alike, and an unattended run hung on it. The old code with the current test file needed 622 seconds for the file, until I cancelled the prompt; the same file now takes 10 seconds. Smart card certificates in the personal store of an administrator are common, so this had to go before the merge.

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: Kept, key container shared with <thumbprint> in Cert:\LocalMachine\My. The help says both.

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 -DeleteKey keeps the container and names the second certificate, whose key still opens; removing the second deletes the container and the file is gone. The smart card prompt itself cannot be reproduced on CI, the lab evidence above stands in for it.

Verification: 13/13 in the lab on PowerShell 7.6 and Windows PowerShell 5.1 with the smart card certificate present in CurrentUser\My, stores clean afterwards. CI is running.

created by Claude and reviewed by Andreas Jordan

@potatoqualitee

Copy link
Copy Markdown
Member

I’d request changes. I reviewed head f7ad6b9 and found one safety blocker and one lower-severity cleanup issue.

1. [P1] The shared-key scan misses archived certificates

Location: public/Remove-DbaComputerCertificate.ps1:322–323

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 OpenFlags.IncludeArchived specifically to include them. Consequently, a readable store can contain another certificate referencing the key without either $sharedWith or $unreadableFolders recording a problem. ([Microsoft Learn]1)

Failure scenario: Extend the existing shared-certificate test by marking the remaining copy in WebHosting as Archived = $true. Remove the unarchived My copy with -DeleteKey. The scan misses the archived copy and proceeds to delete their shared key, leaving the archived certificate installed but unable to use that key. Archiving is a persisted certificate property, not removal of the certificate or its key. This outcome follows from the current code and documented enumeration behavior; I have not run this Windows reproduction locally. ([Microsoft Learn]2)

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 Kept, shared with ..., and can still open its private key afterward. The archived-certificate case is not covered by the current tests. ([Microsoft Learn]3)

2. [P2] Matching public keys does not prove the certificates share a key container

Location: public/Remove-DbaComputerCertificate.ps1:331–335

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:

  1. Removing the machine certificate with -DeleteKey keeps its container because the user certificate has the same public key.
  2. Removing the user certificate afterward deletes only the user container.
  3. The machine container remains orphaned, and its certificate is already gone, so rerunning the command by thumbprint cannot clean it up.

Fix: Preserve the no-smart-card-prompt behavior, but distinguish actual container references using certificate metadata. CertGetCertificateContextProperty(CERT_KEY_PROV_INFO_PROP_ID) exposes provider, container name, machine/user scope, and key specification without acquiring another certificate’s private key. Use that information to distinguish separate containers, retaining a conservative fallback when identity cannot be established. Add a regression with identical public keys in different containers, rather than another .Add() copy of an existing key reference. ([Microsoft Learn]1)

Verification and merge recommendation

The earlier WebHosting/custom-store, unreadable-store, and cross-location issues are addressed in this revision and have regression coverage. The latest ci-azure run for f7ad6b9 completed successfully.

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>
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

Both points addressed in d4da66e.

1. Archived certificates: every folder the scan opens now includes archived certificates (ReadOnly, OpenExistingOnly, IncludeArchived). The certificate to remove is found by its thumbprint even when it is archived, so the copy the output names can be removed with the command afterwards; the help says so under -Thumbprint. Test: a self-signed certificate copied into LocalMachine\TrustedPeople and archived there. Removing the My copy with -DeleteKey keeps the key, reports Kept, shared with <thumbprint> in Cert:\LocalMachine\TrustedPeople, and the archived copy still opens its key; removing the archived copy afterwards deletes the key and the file is gone. On f7ad6b9 the first removal reports Deleted.

2. Container identity instead of the public key: the scan now reads CERT_KEY_PROV_INFO_PROP_ID of every other certificate through CertGetCertificateContextProperty (a small Add-Type wrapper inside the script block). That is a property of the certificate and does not touch the provider, so the smart card certificate in CurrentUser\My stays silent: 0 ms per certificate in both editions with that certificate present. Another certificate keeps the key only when its reference names the same container: the same key set (machine or user), the same container name and the same provider family. Two things the lab showed on the way:

  • Every legacy Microsoft RSA provider (Base, Enhanced, Strong, SChannel, Enhanced RSA and AES) opens the same container by name, so for a legacy key the provider name is not part of the identity; for a Key Storage Provider key it is.
  • A machine certificate stores a legacy container under its unique name (the key file name) while the CNG bridge reports the friendly name, so both names of the key being removed count. The machine/user flag is what separates the two PFX containers, because they share both names.

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 CurrentUser\My with UserKeySet, PersistKeySet, the same PFX file imported once for the machine and once for the user. Removing the machine certificate with -DeleteKey reports Deleted, the machine key file is gone, the user copy still opens its own key and its file under RSA\<SID> is still there; removing the user copy afterwards deletes that one too. On f7ad6b9 the first removal reports Kept, shared with <thumbprint> in Cert:\CurrentUser\My.

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>
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

One follow-up on the test, 78b3e70: on d4da66e the ci-azure lane failed the new separate-container test, and only that test, because the fixture looked for the user key file in the roaming profile. That runner is a LocalSystem service, and Windows keeps the user keys of LocalSystem and the other service accounts under ProgramData\Microsoft\Crypto\RSA\<SID> (verified in the lab with a scheduled task running as SYSTEM). The fixture now looks in both places; the command was not touched. CI is green on 78b3e70.

created by Claude and reviewed by Andreas Jordan

@potatoqualitee

Copy link
Copy Markdown
Member

so close!

I’d still request changes. Reviewed head 78b3e70. The previous findings are addressed, but I found one remaining key-deletion safety issue.

P1: A legacy key referenced through CNG metadata can be deleted while another certificate still uses it

Location: public/Remove-DbaComputerCertificate.ps1, lines 416–417.

The shared-container check rejects any remaining certificate with provider type 0 when the target key belongs to a legacy provider:

} 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 CopyWithPrivateKey handles an RSACng by calling CopyWithPersistedCngKey. That implementation preserves the existing key’s container name, provider name, and machine-key flag, but leaves dwProvType at its default value of zero. It explicitly accommodates legacy CAPI keys accessed through CNG. Consequently, provider type 0 does not prove that this certificate references a different physical container.

Regression scenario to add:

  1. Create a legacy CSP certificate using New-DbaComputerCertificate, then obtain its RSACng through GetRSAPrivateKey.
  2. Create a public-only copy of the certificate, attach that same RSA object using CopyWithPrivateKey, and add the resulting certificate to LocalMachine\TrustedPeople.
  3. Remove the original from LocalMachine\My with -DeleteKey.

Tracing the current code, the remaining certificate’s provider type is 0, so the scan skips it and proceeds to delete their shared legacy container. The TrustedPeople certificate remains installed but loses its persisted private key. This is a source-verified failure path, not a Windows reproduction I executed here.

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 review

The 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 ci-azure, report success. I reviewed the implementation and tests but did not run the Windows integration suite or submit a review on GitHub.

…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>
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

Confirmed and fixed in e8900b2. I built the scenario in the lab on both editions (pwsh 7.6 and Windows PowerShell 5.1): GetRSAPrivateKey of a New-DbaComputerCertificate certificate comes back as an RSACng in both, and CopyWithPrivateKey writes a key reference with the provider name and the container (friendly) name of the legacy key, provider type 0 and key spec 0. With 78b3e70 the removal of the original reported Deleted, the key file was gone, and the copy in LocalMachine\TrustedPeople failed with "Keyset does not exist" on pwsh and "Invalid provider type specified" on 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 HKLM\SOFTWARE\Microsoft\Cryptography\Defaults\Provider, where only the legacy CSPs are registered (the Software and Smart Card KSPs are not; an empty provider name resolves to the parent key and counts as legacy, so it keeps the key). The key set and container name checks are unchanged, so a container of its own is still a container of its own, and no other certificate's key is opened.

Regression test: the CNG bridge copy in LocalMachine\TrustedPeople keeps the key (Kept, shared with <thumbprint> in Cert:\LocalMachine\TrustedPeople), the key file survives, the copy reopens its key and signs with it, and removing the copy afterwards deletes the key. 16/16 on both editions in the lab; on 78b3e70 the new test fails with Deleted. CI is running.

created by Claude and reviewed by Andreas Jordan

@potatoqualitee

Copy link
Copy Markdown
Member

Passed! 🥳

@potatoqualitee
potatoqualitee merged commit 3415c1d into development Sep 23, 2026
21 checks passed
@potatoqualitee
potatoqualitee deleted the feature-remove-dbacomputercertificate-deletekey branch September 23, 2026 11:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants