# The SSL connection could not be established (.NET): fixes

> HttpClient wraps every TLS failure in this message. Read the inner AuthenticationException, then fix the trust, name, protocol or network issue.

Source: https://ssl.sh/errors/dotnet-ssl-connection-could-not-be-established/
Last updated: 2026-11-10

**`System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.` means the TLS handshake failed; HttpClient uses this wrapper for every TLS problem.** The inner exception holds the cause: a rejected certificate, a name mismatch, no common protocol or a dropped connection. Fix that cause rather than using `DangerousAcceptAnyServerCertificateValidator`.

## What the error means

The inner exception, not this wrapper, holds the diagnosis. HttpClient (through `SocketsHttpHandler` and `SslStream`) raises this `HttpRequestException` whenever authentication fails. The text comes from the `net_http_ssl_connection_failed` resource in .NET. Below it you will find one of these inner exceptions, which are the real diagnosis:

```text
AuthenticationException: The remote certificate is invalid according to the validation procedure: RemoteCertificateChainErrors
AuthenticationException: The remote certificate is invalid according to the validation procedure: RemoteCertificateNameMismatch
AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot
AuthenticationException: Authentication failed because the remote party has closed the transport stream.
AuthenticationException: Authentication failed because the remote party sent a TLS alert: 'HandshakeFailure'.
Win32Exception (0x80090326): The message received was unexpected or badly formatted.
```

Microsoft's SslStream troubleshooting guide points out that .NET relies on the operating system: Schannel on Windows, OpenSSL on Linux. The same code can therefore fail on one platform and work on another, with different inner messages.

## Causes, most common first

### 1. The certificate chain is not trusted

`RemoteCertificateChainErrors` with `UntrustedRoot` or `PartialChain`: the server uses a private CA, a TLS-inspecting proxy re-signs the traffic, or the server does not send its intermediate certificate.

- **Confirm:** check the chain from outside with the [SSL checker](https://ssl.sh/tools/ssl-checker/) or [chain builder](https://ssl.sh/tools/chain-builder/). If the public view is fine, the client's trust store is missing a root, typically inside a Linux container.
- **Fix:** add the root to the platform store (commands below), or trust it for one client with `CertificateChainPolicy`. Missing intermediates must be fixed on the server ([missing intermediate certificate](https://ssl.sh/errors/missing-intermediate-certificate/)).

### 2. The host name does not match

`RemoteCertificateNameMismatch`: you call an IP address, an internal alias or the bare domain while the certificate covers other names.

- **Confirm:** `openssl s_client -connect example.com:443 -servername example.com </dev/null | openssl x509 -noout -ext subjectAltName`.
- **Fix:** call a name the certificate covers or reissue the certificate. More detail: [.NET remote certificate is invalid](https://ssl.sh/errors/dotnet-remote-certificate-is-invalid/).

### 3. No common protocol or cipher suite

The inner exception shows `remote party has closed the transport stream`, a `HandshakeFailure` alert, or on Windows a Schannel error. Causes include a server stuck on TLS 1.0, a server that requires TLS 1.3 while Windows 10 tops out at TLS 1.2 in Schannel, or a container whose `/etc/ssl/openssl.cnf` disables what the server offers.

- **Confirm:** `openssl s_client -connect example.com:443 -servername example.com -tls1_2` and `-tls1_3`; on Linux, read `/etc/ssl/openssl.cnf` for `MinProtocol` and `CipherString`.
- **Fix:** upgrade the server's TLS configuration; do not pin `SslProtocols` in code.

### 4. A firewall or proxy drops the connection

The same request works from another network, or only fails behind the corporate proxy. Configure the proxy for HttpClient (`HttpClient.DefaultProxy` or the `HTTPS_PROXY` variable) and trust the proxy's root if it inspects TLS ([corporate SSL inspection](https://ssl.sh/errors/corporate-ssl-inspection/)).

### 5. Client certificate and Windows specifics

With mutual TLS, a client certificate whose private key the process cannot use fails the handshake; Microsoft documents `0x8009030E: No credentials are available in the security package` for certificates with ephemeral keys on Windows. Large certificate chains can also exceed Schannel's handshake message limit (`0x80090326`). See [Windows Schannel errors](https://ssl.sh/errors/windows-schannel-errors/).

## Fix commands by platform

Most fixes add the missing root to the trust store that .NET reads on that platform.

**Windows (elevated PowerShell):**

```powershell
Import-Certificate -FilePath C:\certs\company-root.cer -CertStoreLocation Cert:\LocalMachine\Root
```

**Debian / Ubuntu images:**

```dockerfile
COPY company-root.crt /usr/local/share/ca-certificates/company-root.crt
RUN update-ca-certificates
```

**RHEL / Alpine:** `update-ca-trust` after copying into `/etc/pki/ca-trust/source/anchors/` (RHEL), or `update-ca-certificates` (Alpine). On any Linux you can also point `SSL_CERT_FILE` at a PEM bundle; Microsoft's docs confirm .NET honours `SSL_CERT_FILE` and `SSL_CERT_DIR`.

**macOS:**

```bash
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain company-root.cer
```

**In code, one client only (.NET 7+):**

```csharp
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

var root = X509Certificate2.CreateFromPemFile("company-root.pem");
var handler = new SocketsHttpHandler
{
    SslOptions = new SslClientAuthenticationOptions
    {
        CertificateChainPolicy = new X509ChainPolicy
        {
            TrustMode = X509ChainTrustMode.CustomRootTrust,
            CustomTrustStore = { root },
        },
    },
};
using var client = new HttpClient(handler);
```

The host name is still checked, and every other root is rejected for this client, which is exactly what you want for an internal service.

## Don't disable certificate verification

`handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;` or a callback that returns `true` accepts any certificate, including one forged by an attacker on the network path. Microsoft's API documentation describes it as a pattern for test scenarios and notes that its name exists so tools can flag the danger. In production it exposes bearer tokens, passwords and API data.

The safer alternatives are the ones above: install the root where the platform looks for it, use `CertificateChainPolicy` with `CustomRootTrust` for a single client, fix the server chain, and correct host names. If you need diagnostics, write a callback that **logs** `sslPolicyErrors` and `chain.ChainStatus` and still returns `sslPolicyErrors == SslPolicyErrors.None`.

## Verify the fix

```csharp
using var client = new HttpClient();
var response = await client.GetAsync("https://example.com/");
Console.WriteLine($"{(int)response.StatusCode} {response.Version}");
```

The request should return a status code with no `HttpRequestException`. Run it on the same OS image as production, because Windows and Linux trust stores differ. Re-check the server in the [SSL checker](https://ssl.sh/tools/ssl-checker/), and see [certificate chains explained](https://ssl.sh/learn/certificate-chains/) if the inner exception mentioned `PartialChain`.
