# The SSL connection could not be established（.NET）怎么修

> HttpClient 把所有 TLS 失败都包装成这条消息。先读 InnerException，再分别修信任、域名、协议或连接问题，不要用 DangerousAcceptAnyServerCertificateValidator。

Source: https://ssl.sh/zh/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.` 的意思是 TLS 握手失败了；HttpClient 对所有 TLS 问题都用这同一句话包装。** 这句话本身没有更多信息，要看内部异常：它会说明是证书被拒绝、域名不匹配、协议没有交集，还是连接被掐断了。按原因去修，不要直接上 `DangerousAcceptAnyServerCertificateValidator`。

## 这条报错是什么意思

HttpClient（通过 `SocketsHttpHandler` 和 `SslStream`）只要身份验证失败，就会抛出这个 `HttpRequestException`，文字来自 .NET 的 `net_http_ssl_connection_failed` 资源。诊断信息在它下面挂着的内部异常里，常见的有这些：

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

微软的 SslStream 排错文档指出，.NET 的 TLS 依赖操作系统：Windows 上是 Schannel，Linux 上是 OpenSSL。所以同一份代码可能在一个平台正常、在另一个平台失败，内部异常的文字也不一样。

排查分三步：先打印完整异常链，确认最里层是证书问题、协议问题还是连接被断开；再在出问题的机器（或容器）里用 `openssl s_client` 连同一个地址，看证书链和协商出的协议；最后对照下面的原因逐条核对。跳过第一步、直接改代码里的协议或回调，问题只会更难查。

## 常见原因（按出现频率排序）

这个异常有 5 类原因，最常见的是证书链不受信任，Linux 容器里缺根证书尤其多。

### 1. 证书链不受信任

`RemoteCertificateChainErrors`，并带有 `UntrustedRoot` 或 `PartialChain`：服务器用的是私有 CA，有 HTTPS 解密代理在重新签名流量，或者服务器没发中间证书。

- **确认：** 用 [SSL 检测工具](https://ssl.sh/zh/tools/ssl-checker/)或[证书链补全工具](https://ssl.sh/zh/tools/chain-builder/)从外部检查证书链。外部看没问题，就是客户端信任库缺根证书，Linux 容器里最常见。
- **修复：** 把根证书加入平台证书库（命令见下文），或者用 `CertificateChainPolicy` 只让一个客户端信任它。缺中间证书则必须在服务器上修（见[缺少中间证书](https://ssl.sh/zh/errors/missing-intermediate-certificate/)）。

### 2. 域名不匹配

`RemoteCertificateNameMismatch`：你访问的是 IP、内部别名或者裸域名，而证书覆盖的是别的名字。

- **确认：** `openssl s_client -connect example.com:443 -servername example.com </dev/null | openssl x509 -noout -ext subjectAltName`。
- **修复：** 改用证书覆盖的名字，或重新签发证书。详见 [.NET remote certificate is invalid](https://ssl.sh/zh/errors/dotnet-remote-certificate-is-invalid/)。

### 3. 没有共同的协议或加密套件

`remote party has closed the transport stream`、`HandshakeFailure` 告警，或者 Windows 上的 Schannel 错误。可能是服务器还停留在 TLS 1.0；服务器只接受 TLS 1.3，而 Windows 10 的 Schannel 最高只到 TLS 1.2；或者容器里的 `/etc/ssl/openssl.cnf` 禁用了服务器提供的东西。

- **确认：** `openssl s_client -connect example.com:443 -servername example.com -tls1_2` 和 `-tls1_3`；Linux 上查看 `/etc/ssl/openssl.cnf` 里的 `MinProtocol` 和 `CipherString`。
- **修复：** 升级服务器的 TLS 配置；不要在代码里写死 `SslProtocols`。

### 4. 防火墙或代理掐断了连接

换个网络同样的请求就正常，或者只在公司代理后面失败。给 HttpClient 配置代理（`HttpClient.DefaultProxy` 或 `HTTPS_PROXY` 环境变量），如果代理会解密 HTTPS，还要信任它的根证书（见[企业 SSL 解密导致的报错](https://ssl.sh/zh/errors/corporate-ssl-inspection/)）。

### 5. 客户端证书和 Windows 特有问题

双向 TLS 时，如果进程用不了客户端证书的私钥，握手就会失败；微软文档记录了 Windows 上使用临时密钥证书时出现的 `0x8009030E: No credentials are available in the security package`。证书链很长时还可能超过 Schannel 的握手消息大小限制（`0x80090326`）。见 [Windows Schannel 报错](https://ssl.sh/zh/errors/windows-schannel-errors/)。

## 分平台修复命令

各平台的修法都是把私有根证书或代理根证书装进 .NET 实际读取的信任库：Windows 证书存储、Linux 的 CA 证书包或 macOS 系统钥匙串。

**Windows（管理员 PowerShell）：**

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

**Debian / Ubuntu 镜像：**

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

**RHEL / Alpine：** RHEL 系把证书复制到 `/etc/pki/ca-trust/source/anchors/` 后执行 `update-ca-trust`；Alpine 执行 `update-ca-certificates`。任何 Linux 上都可以让 `SSL_CERT_FILE` 指向一个 PEM 证书包，微软文档确认 .NET 会读取 `SSL_CERT_FILE` 和 `SSL_CERT_DIR`。

**macOS：**

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

**代码里只对一个客户端生效（.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);
```

域名仍然会校验，而且这个客户端只信任这一张根证书，适合访问内网服务。

**国内环境：** 用阿里云、腾讯云的容器服务或 CI 构建镜像时，基础镜像里通常只有公开根证书；访问公司内网 HTTPS 服务前，要在 Dockerfile 里把内网根证书装进去。拉取 NuGet 包慢的问题和证书无关，不要因为下载慢就去关闭证书校验。

## 不推荐：关闭证书校验

关闭校验能消掉这个异常，代价是 HttpClient 会接受任何证书。`handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;` 或永远返回 `true` 的回调，连链路上攻击者伪造的证书也会接受。微软的 API 文档把它描述为测试场景下的用法，并说明起这个名字就是为了让工具能标记出危险。用在生产环境，会暴露 Bearer token、密码和接口数据。

更安全的替代办法有 4 种：把根证书装到平台查找的位置；用 `CertificateChainPolicy` 加 `CustomRootTrust` 只给一个客户端；修好服务器证书链；改正访问的域名。需要诊断信息时，写一个**只记录** `sslPolicyErrors` 和 `chain.ChainStatus`、仍然返回 `sslPolicyErrors == SslPolicyErrors.None` 的回调。

## 怎么确认已经修好

修好的标准是：在和生产环境相同的系统镜像上运行下面这段代码，能打印出状态码。

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

修好后，请求会返回状态码，不再抛出 `HttpRequestException`。Windows 和 Linux 的信任库不同，所以必须在相同镜像上测。容器场景下，最好把这段检查写进镜像的健康检查或启动自检里：镜像一旦漏装根证书，部署时就能立刻发现，而不是等到线上请求失败。修好之后，把排查期间临时加的日志回调、环境变量清理掉，尤其是任何返回 `true` 的证书回调。再用 [SSL 检测工具](https://ssl.sh/zh/tools/ssl-checker/)复查服务器；内部异常里出现 `PartialChain` 时，可以读[证书链原理](https://ssl.sh/zh/learn/certificate-chains/)。
