编程语言

The SSL connection could not be established(.NET)怎么修

作者: · 最后核实

报错信息The SSL connection could not be established, see inner exception.

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 资源。诊断信息在它下面挂着的内部异常里,常见的有这些:

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 检测工具或证书链补全工具从外部检查证书链。外部看没问题,就是客户端信任库缺根证书,Linux 容器里最常见。
  • 修复: 把根证书加入平台证书库(命令见下文),或者用 CertificateChainPolicy 只让一个客户端信任它。缺中间证书则必须在服务器上修(见缺少中间证书)。

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。

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 解密导致的报错)。

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

双向 TLS 时,如果进程用不了客户端证书的私钥,握手就会失败;微软文档记录了 Windows 上使用临时密钥证书时出现的 0x8009030E: No credentials are available in the security package。证书链很长时还可能超过 Schannel 的握手消息大小限制(0x80090326)。见 Windows Schannel 报错。

分平台修复命令

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

Windows(管理员 PowerShell):

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

Debian / Ubuntu 镜像:

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:

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

代码里只对一个客户端生效(.NET 7+):

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 的回调。

怎么确认已经修好

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

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 检测工具复查服务器;内部异常里出现 PartialChain 时,可以读证书链原理。

常见问题

内部异常在哪里看?

记录 ex.ToString() 而不是 ex.Message,它会打印完整的异常链。代码里可以沿着 ex.InnerException 一直往下找到 null 为止。最里层的异常(通常是 AuthenticationException,或带错误码的 Win32Exception)才写明具体原因。

Windows 本机正常,放进 Linux 容器就报错,为什么?

Windows 上的 .NET 用 Windows 证书存储和 Schannel,Linux 上用 OpenSSL 和容器里的 CA 证书包。公司通过域策略下发到 Windows 的根证书,容器里并没有。用 update-ca-certificates 把它加进镜像,或者让 SSL_CERT_FILE 指向包含它的证书包。容器里的 /etc/ssl/openssl.cnf 也可能禁用了服务器需要的协议或套件。

要不要在代码里显式设置 SslProtocols?

通常不要。微软建议让操作系统选择 TLS 版本,写死 SslProtocols.Tls12 以后反而会挡住 TLS 1.3。只有合规要求时才限制协议,并且永远不要包含 SSL 3 或 TLS 1.0。

Authentication failed because the remote party has closed the transport stream 是什么意思?

服务器或中间设备在握手过程中直接关闭了 TCP 连接,而不是正常回应。常见原因是协议或加密套件不匹配、没有发送 SNI、或者防火墙拦截。用 openssl s_client -connect host:443 -servername host -tls1_2 和 -tls1_3 测试服务器接受哪些版本。

资料来源

  1. Microsoft Learn: Troubleshoot SslStream authentication issues
  2. Microsoft Learn: Cross-platform cryptography (trusted root locations on Linux)
  3. Microsoft Learn: SslClientAuthenticationOptions.CertificateChainPolicy
  4. Microsoft Learn: HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
  5. dotnet/runtime: System.Net.Http Strings.resx