- Windows `tlsCertificateSelector: subject=...` selects first matching cert only
-
- Summary
On Windows, MongoDB's TLS certificate selector can select the wrong certificate when
`tlsCertificateSelector` uses `subject=...` and the Windows `My` certificate store contains multiple
certificates with the same subject.
The current implementation calls `CertFindCertificateInStore()` once with
`pPrevCertContext = NULL`, accepts the first returned matching certificate, and only later validates
whether that certificate is expired or not yet valid.
If the first matching certificate is expired, MongoDB fatals during startup, even if another
matching certificate with the same subject is valid and has an accessible private key.
-
- Affected Area
- MongoDB server TLS certificate selection
- Windows only
- Windows certificate store / Schannel path
- `tlsCertificateSelector: subject=...`
Observed in `mongodb/mongo` `master` at:
```text
aee5a98a
```
-
- Current Behavior
With a config like:
```yaml
net:
tls:
mode: requireTLS
certificateSelector: "subject=example.domain.local"
```
MongoDB selects the first certificate returned by Windows CryptoAPI for that subject.
If that first matching certificate is expired, MongoDB fails startup with:
```text
The provided SSL certificate is expired or not yet valid.
```
MongoDB does not continue searching for another certificate with the same subject.
-
- Expected Behavior
When selecting by `subject=...` on Windows, MongoDB should search all matching certificates and
choose a usable certificate.
A usable certificate should at least:
- Match the requested subject
- Be currently valid according to `NotBefore` / `NotAfter`
- Have an accessible private key
If multiple matching certificates exist, an expired matching certificate should not prevent MongoDB
from selecting a later valid matching certificate.
-
- Current Code Path
File:
```text
src/mongo/util/net/ssl_manager_windows.cpp
```
Function:
```cpp
StatusWith<UniqueCertificate> loadCertificateSelectorFromStore(
SSLParams::CertificateSelector selector, DWORD storeType, std::string_view storeName)
```
Current subject-selector branch:
```cpp
if (!selector.subject.empty()) {
std::wstring wstr = toNativeString(selector.subject.c_str());
PCCERT_CONTEXT cert = CertFindCertificateInStore(storeHolder,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
0,
CERT_FIND_SUBJECT_STR,
wstr.c_str(),
NULL);
if (!cert)
return UniqueCertificate(cert);
}
```
The important part is the final argument:
```cpp
NULL
```
This means MongoDB asks CryptoAPI for the first matching certificate in the store. MongoDB then
immediately accepts that certificate:
```cpp
return UniqueCertificate(cert);
```
There is no loop over additional matching certificates.
-
- Where Startup Fails
Later, the selected certificate is validated in:
```cpp
Status SSLManagerWindows::_validateCertificate(PCCERT_CONTEXT cert,
SSLX509Name* subjectName,
Date_t* serverCertificateExpirationDate)
```
Relevant code:
```cpp
if ((FiletimeToULL(cert->pCertInfo->NotBefore) > currentTimeLong) ||
(currentTimeLong > FiletimeToULL(cert->pCertInfo->NotAfter)))
```
So the current behavior is:
```text
1. Find first cert matching subject.
2. Return it immediately.
3. Later validate it.
4. If it is expired, fatal startup.
5. Never try the next matching subject cert.
```
-
- Why This Is Windows-Specific
This path is specific to:
```text
src/mongo/util/net/ssl_manager_windows.cpp
```
The Windows certificate selector uses CryptoAPI / Schannel and searches the Windows `My`
certificate store. Linux/OpenSSL deployments generally use PEM files directly, so this specific
duplicate-subject Windows store behavior does not occur in the same way.
-
- Proof of Behavior
I reproduced the behavior with a small Windows CryptoAPI harness using the same API pattern MongoDB
uses:
```cpp
CertFindCertificateInStore(
store,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
0,
CERT_FIND_SUBJECT_STR,
subject,
NULL);
```
Test setup:
```text
- Create two certificates with the same subject.
- Certificate 1: expired.
- Certificate 2: valid.
- Search by subject using CertFindCertificateInStore.
```
Current MongoDB-shaped behavior:
```text
Current MongoDB shape, one CertFind call:
selected: CN=MongoSelectorHarness-... | NotBefore=2026-06-05 | NotAfter=2026-06-15 | expired
```
Fixed loop behavior:
```text
Fixed shape, repeated CertFind calls with previous context:
candidate 1: CN=MongoSelectorHarness-... | NotAfter=2026-06-15 | expired -> skip
candidate 2: CN=MongoSelectorHarness-... | NotAfter=2026-07-25 | valid -> select
result: CN=MongoSelectorHarness-... | valid
```
This demonstrates that the current one-call selector can return the expired certificate, while using
the documented `pPrevCertContext` iteration pattern allows MongoDB to find the later valid
certificate.
-
- Proposed Fix
Change the `subject=...` branch in `loadCertificateSelectorFromStore()` to loop over all matching
certificates.
Instead of:
```cpp
PCCERT_CONTEXT cert = CertFindCertificateInStore(..., wstr.c_str(), NULL);
return UniqueCertificate(cert);
```
Use the previous certificate context to continue the search:
```cpp
PCCERT_CONTEXT previous = nullptr;
while (true) {
PCCERT_CONTEXT cert = CertFindCertificateInStore(
storeHolder,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
0,
CERT_FIND_SUBJECT_STR,
wstr.c_str(),
previous);
previous = cert;
if (!cert)
{ break; }if (cert is expired or not yet valid)
{ continue; }if (cert does not have an accessible private key) { continue; }
return UniqueCertificate(CertDuplicateCertificateContext(cert));
}
```
The existing private-key validation currently happens after selection in:
```cpp
loadAndValidateCertificateSelector()
```
That logic could be extracted into a helper and reused while iterating subject candidates.
-
- Important Design Point
For `thumbprint=...`, the current behavior should probably remain strict.
If the user selects an exact thumbprint, MongoDB should select exactly that certificate and fail if
that exact certificate is expired or unusable.
The issue is specifically with `subject=...`, because subject is not unique.
-
- Operational Impact
This affects Windows deployments using certificate selectors such as:
```yaml
net:
tls:
mode: requireTLS
certificateSelector: "subject=example.domain.local"
```
A common operational scenario is certificate renewal where the old expired certificate remains in
`LocalMachine\My` or `CurrentUser\My` alongside the renewed certificate. If both certificates share
the same subject, MongoDB may select the expired certificate first and fail startup.
-
- Workarounds
Use `thumbprint=...` instead of `subject=...`, pointing at the renewed valid certificate:
```yaml
net:
tls:
certificateSelector: "thumbprint=<valid_certificate_thumbprint>"
```
Alternatively, remove the expired duplicate-subject certificate from the Windows `My` certificate
store.
-
- Suggested Fix Summary
`loadCertificateSelectorFromStore()` should not treat the first `CERT_FIND_SUBJECT_STR` match as
authoritative.
It should iterate all matching subject certificates with:
```cpp
CertFindCertificateInStore(..., previousContext)
```
Then it should skip expired, not-yet-valid, or otherwise unusable candidates and return the first
usable candidate.