# The remote certificate is invalid (.NET): causes and fixes

> .NET throws this AuthenticationException when SslStream rejects a server certificate. Read the SslPolicyErrors value, then fix the chain, name or trust store.

Source: https://ssl.sh/errors/dotnet-remote-certificate-is-invalid/
Last updated: 2026-11-10

**`The remote certificate is invalid according to the validation procedure` means .NET's `SslStream` rejected the server's certificate during the TLS handshake.** `HttpClient` usually wraps it in `HttpRequestException: The SSL connection could not be established, see inner exception`. The real reason is in the `SslPolicyErrors` flags: an untrusted or incomplete chain, a name mismatch, or no certificate at all.

Newer .NET versions often append the reason to the message, for example `...: RemoteCertificateNameMismatch, RemoteCertificateChainErrors` or `The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot`.

## Quick fix

The SslPolicyErrors flag tells you which fix applies.

Find out which flag is set. Check the server from a clean machine first:

```bash
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
```

Then match the flag to the fix:

| SslPolicyErrors / chain status | Meaning | Fix |
|---|---|---|
| `RemoteCertificateChainErrors` + `PartialChain` | Server omitted an intermediate | Send the full chain from the server |
| `RemoteCertificateChainErrors` + `UntrustedRoot` | Root not trusted on this machine | Install the private or proxy root |
| `RemoteCertificateChainErrors` + `NotTimeValid` | Expired or not-yet-valid | Renew; check the client clock |
| `RemoteCertificateNameMismatch` | Host not in the certificate SAN | Use the right host or reissue |
| `RemoteCertificateNotAvailable` | No certificate presented | Check the server's TLS setup |

The [SSL checker](https://ssl.sh/tools/ssl-checker/) shows chain, names and dates for public hosts in one report.

## Causes

### 1. Untrusted root (private CA, self-signed, proxy)

Internal APIs, corporate TLS-inspecting proxies and local development servers use roots that are not in the public store.

- **Confirm:** chain status `UntrustedRoot`; the top issuer is your company or proxy.
- **Fix:** install the root into the machine trust store (below). If only one application should trust it, validate against that root explicitly:

```csharp
var companyRoot = X509Certificate2.CreateFromPemFile("company-root.pem");
var handler = new SocketsHttpHandler();
handler.SslOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
{
    if (errors == SslPolicyErrors.None) return true;
    if (errors != SslPolicyErrors.RemoteCertificateChainErrors || cert is not X509Certificate2 leaf)
        return false;                       // never ignore name mismatches
    using var custom = new X509Chain();
    custom.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
    custom.ChainPolicy.CustomTrustStore.Add(companyRoot);
    return custom.Build(leaf);
};
var client = new HttpClient(handler);
```

This still enforces host name checks and only adds one specific root.

### 2. Incomplete chain on the server

- **Confirm:** chain status `PartialChain`; `openssl s_client` shows a single certificate.
- **Fix:** configure leaf plus intermediates on the server or load balancer. In IIS, import the intermediate into the computer's "Intermediate Certification Authorities" store. See [missing intermediate certificate](https://ssl.sh/errors/missing-intermediate-certificate/).

### 3. Name mismatch

- **Confirm:** `RemoteCertificateNameMismatch`; you connect by IP, by an internal alias, or through a host header the certificate does not cover.
- **Fix:** connect with a name listed in the certificate's [SAN](https://ssl.sh/glossary/san/), or reissue it.

### 4. Expired certificate or wrong clock

- **Confirm:** `NotTimeValid`. Check the server's expiry and the client's clock, which matters for VMs restored from snapshots.

## Fixes by environment

On every OS, the fix is to trust the right root in the store .NET reads, or to fix the server's chain or name.

**Windows (.NET Framework and .NET):** import the root into "Trusted Root Certification Authorities" for the Local Computer (`certlm.msc`) or with PowerShell:

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

**Linux and containers:** .NET reads the OpenSSL store.

```dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0
COPY company-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates
```

For Alpine-based images install `ca-certificates` with `apk` first.

**macOS:** add the root to the System keychain and mark it "Always Trust".

**ASP.NET Core local development:** `dotnet dev-certs https --trust` trusts the development certificate on Windows and macOS.

**.NET Framework 4.x:** if the inner message instead says the connection was closed unexpectedly, the app may be offering only old TLS versions; that is a protocol problem, not this error.

## Don't do this

- **`ServicePointManager.ServerCertificateValidationCallback = (s, c, ch, e) => true;`** in .NET Framework. It disables validation for every connection in the process.
- **`HttpClientHandler.DangerousAcceptAnyServerCertificateValidator`** outside isolated tests.
- **Callbacks that ignore `RemoteCertificateNameMismatch`.** Trusting a root is reasonable; accepting any name is not, because any certificate from that root could impersonate your server.
