Skip to content

Managed Identity V2: Inaccessible KeyGuard-backed certificate causes mTLS failures with no automatic recovery #5755

@gladjohn

Description

@gladjohn

Library version used

4.61.0

.NET version

.NET 8.0.23, Windows 10.0.20348

Scenario

ManagedIdentityClient - managed identity

Is this a new or an existing app?

No response

Issue description and reproduction steps

Issue details

In Managed Identity V2 scenarios, certificates are stored in the Windows certificate store and private keys are protected by KeyGuard.

If KeyGuard becomes inaccessible (due to machine restart, corruption, VBS failure), the certificate may remain while its private key reference is broken. This results in mTLS handshake failures caused by the inability to access the private key, producing errors like:

System.Net.Http.HttpRequestException: An error occurred while sending the request.
---> System.Net.Sockets.SocketException (10054): An existing connection was forcibly closed by the remote host.

Current logic only checks cert.HasPrivateKey and does not validate key accessibility before using a cached certificate. As a result, both persistent and in-memory caches may retain unusable certificates, leading to repeated failures and service disruptions with no automatic remediation.

Relevant code snippets

try {
    using var rsa = cert.GetRSAPrivateKey();
    byte[] data = Encoding.UTF8.GetBytes("test");
    rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
} catch {
    // On failure, delete cert from cert store and clear in-memory cache, fetch new cert
}

Expected behavior

Before using cached certificates, MSAL should proactively validate the accessibility of the private key by attempting a signing operation. If validation fails, the certificate should be deleted from both persistent and in-memory caches, and a fresh certificate provisioned. Fallback to non-KeyGuard keys or retrying with exponential backoff should be avoided.

Identity provider

Microsoft Entra ID (Work and School accounts and Personal Microsoft accounts)

Regression

No response

Solution and workarounds

Add key accessibility checks at cache retrieval points in MtlsCertificateCache and WindowsPersistentCertificateCache. On validation failure, delete from all caches and trigger fresh certificate provisioning in managed identity scenarios.

Error Logs

this is the error

AL] True MSAL 4.61.0.0 MSAL.NetCore .NET 8.0.23 Microsoft Windows 10.0.20348 [2026-02-14 17:10:46Z - ffcf10a2-11ad-44f5-a544-b528876bb456] Starting [HttpManager] ExecuteAsync
[MSAL] True MSAL 4.61.0.0 MSAL.NetCore .NET 8.0.23 Microsoft Windows 10.0.20348 [2026-02-14 17:10:46Z - ffcf10a2-11ad-44f5-a544-b528876bb456] [HttpManager] Sending request. Method: POST. URI: https://centraluseuap.mtlsauth.microsoft.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/v2.0/token. Binding Certificate: True. Endpoint: https://centraluseuap.mtlsauth.microsoft.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/v2.0/token
[MSAL] True MSAL 4.61.0.0 MSAL.NetCore .NET 8.0.23 Microsoft Windows 10.0.20348 [2026-02-14 17:10:46Z - ffcf10a2-11ad-44f5-a544-b528876bb456] Finished [HttpManager] ExecuteAsync in 235 ms
[MSAL] True MSAL 4.61.0.0 MSAL.NetCore .NET 8.0.23 Microsoft Windows 10.0.20348 [2026-02-14 17:10:46Z - ffcf10a2-11ad-44f5-a544-b528876bb456] [ManagedIdentityRequest] Released managed identity request semaphore.
[MSAL] True MSAL 4.61.0.0 MSAL.NetCore .NET 8.0.23 Microsoft Windows 10.0.20348 [2026-02-14 17:10:47Z - ffcf10a2-11ad-44f5-a544-b528876bb456] MSAL.NetCore.4.61.0.0.MsalServiceException:
ErrorCode: managed_identity_unreachable_network
Microsoft.Identity.Client.MsalServiceException: An error occurred while sending the request.
---> System.Net.Http.HttpRequestException: An error occurred while sending the request.
---> System.IO.IOException: Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host..
---> System.Net.Sockets.SocketException (10054): An existing connection was forcibly closed by the remote host.
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.CreateException(SocketError error, Boolean forAsyncThrow)
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.SendAsyncForNetworkStream(Socket socket, CancellationToken cancellationToken)
at System.Net.Sockets.Socket.SendAsyncForNetworkStream(ReadOnlyMemory1 buffer, SocketFlags socketFlags, CancellationToken cancellationToken) at System.Net.Sockets.NetworkStream.WriteAsync(ReadOnlyMemory1 buffer, CancellationToken cancellationToken)
at System.Net.Security.SslStream.WriteSingleChunk[TIOAdapter](ReadOnlyMemory1 buffer, CancellationToken cancellationToken) at System.Net.Security.SslStream.WriteAsyncInternal[TIOAdapter](ReadOnlyMemory1 buffer, CancellationToken cancellationToken)
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
at System.Net.Security.SslStream.WriteAsyncInternal[TIOAdapter](ReadOnlyMemory1 buffer, CancellationToken cancellationToken) at System.Net.Http.HttpConnection.WriteToStreamAsync(ReadOnlyMemory1 source, Boolean async)
at System.Net.Http.HttpConnection.FlushAsync(Boolean async)
at System.Net.Http.HttpConnection.SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream, Boolean async, CancellationToken cancellationToken)
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
at System.Net.Http.HttpConnection.SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
at System.Net.Http.HttpConnection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Runtime.CompilerServices.AsyncTaskMethodBuilder1.AsyncStateMachineBox1.ExecutionContextCallback(Object s)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Runtime.CompilerServices.AsyncTaskMethodBuilder1.AsyncStateMachineBox1.MoveNext(Thread threadPoolThread)
at System.Runtime.CompilerServices.AsyncTaskMethodBuilder1.AsyncStateMachineBox1.MoveNext()
at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(IAsyncStateMachineBox box, Boolean allowInlining)
at System.Threading.Tasks.Task.RunContinuations(Object continuationObject)
at System.Threading.Tasks.Task1.TrySetResult(TResult result) at System.Runtime.CompilerServices.AsyncTaskMethodBuilder1.SetExistingTaskResult(Task1 task, TResult result) at System.Threading.Tasks.TaskCompletionSourceWithCancellation1.WaitWithCancellationAsync(CancellationToken cancellationToken)
at System.Runtime.CompilerServices.AsyncTaskMethodBuilder1.AsyncStateMachineBox1.ExecutionContextCallback(Object s)
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Runtime.CompilerServices.AsyncTaskMethodBuilder1.AsyncStateMachineBox1.MoveNext(Thread threadPoolThread)
at System.Runtime.CompilerServices.AsyncTaskMethodBuilder1.AsyncStateMachineBox1.ExecuteFromThreadPool(Thread threadPoolThread)
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
--- End of stack trace from previous location ---

--- End of inner exception stack trace ---
at System.Net.Security.SslStream.g__CompleteWriteAsync|157_1[TIOAdapter](ValueTask writeTask, Byte[] bufferToReturn)
at System.Net.Security.SslStream.WriteAsyncInternal[TIOAdapter](ReadOnlyMemory1 buffer, CancellationToken cancellationToken) at System.Net.Http.HttpConnection.SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) --- End of inner exception stack trace --- at System.Net.Http.HttpConnection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken) at Microsoft.Identity.Client.Http.HttpManager.ExecuteAsync(Uri endpoint, IDictionary2 headers, HttpContent body, HttpMethod method, X509Certificate2 bindingCertificate, Func5 validateServerCert, ILoggerAdapter logger, CancellationToken cancellationToken) in C:\fix-mtls-handshake-failures-new\src\client\Microsoft.Identity.Client\Http\HttpManager.cs:line 257 at Microsoft.Identity.Client.Http.HttpManager.SendRequestAsync(Uri endpoint, IDictionary2 headers, HttpContent body, HttpMethod method, ILoggerAdapter logger, Boolean doNotThrow, X509Certificate2 bindingCertificate, Func`5 validateServerCert, CancellationToken cancellationToken, IRetryPolicy retryPolicy, Int32 retryCount) in C:\fix-mtls-handshake-failures-new\src\client\Microsoft.Identity.Client\Http\HttpManager.cs:line 85

Metadata

Metadata

Type

Projects

Status

Committed

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions