创建TLS客户端凭据时发生严重错误。内部错误状态为 10013。这个报错我最近又碰到一次,现场是Windows Server 2019 + IIS,业务应用调用外部HTTPS接口时突然全部超时,系统日志里Schannel来源疯狂刷36871。很多人看到TLS、客户端凭据、严重错误这几个词,第一反应是证书过期或者协议版本不对,但10013这个内部状态往往把方向指向另一件事:权限。它不是说TLS协议本身谈不拢,而是Windows在准备客户端凭据时,底层访问被拒绝。客户端凭据可以理解成Windows替应用程序去握手的“身份证”,这张身份证由证书和私钥组成,Schannel在创建它的时候需要读取私钥,如果读取动作被ACL挡在门外,就会抛出内部状态10013。这个错误在IIS、.NET、WinHTTP、SQL Server、Exchange、甚至某些桌面客户端里都可能出现,表现却五花八门:有的服务直接不可用,有的只是间歇性失败,有的只在重启后第一笔请求失败。下面我把这次处理记录拆开讲,包括错误链路、排查顺序、修复动作和踩过的坑,尽量让遇到同类问题的朋友能直接抄作业。
1. 先把10013翻译成人话:它到底卡在哪一步
1.1 Schannel创建客户端凭据的幕后流程
Windows里负责TLS/SSL握手的主要组件是Schannel,它不像OpenSSL那样直接暴露API给应用,而是通过SSPI接口让应用程序调用。当应用程序要作为客户端去连接HTTPS服务时,它会调用AcquireCredentialsHandle,并指定要使用某个证书。Schannel收到请求后,会去证书存储里找到对应证书,然后尝试打开私钥容器,读取私钥句柄,最后组装成客户端凭据。这个过程中任何一步失败,都会在Schannel事件日志里留下记录,事件ID 36871就是典型的一条:“创建 TLS 客户端凭据时发生严重错误。内部错误状态为 10013。”这里的10013不是Win32的“访问被拒绝”码本身,而是Schannel内部状态映射后的结果,但它确实常常对应到底层权限问题。换句话说,证书存在、私钥也存在,但当前进程的身份没有权限打开私钥。很多人会误以为“证书已经导入,IIS能选到,就代表权限没问题”,实际上证书管理单元里能看到证书,只说明当前登录的管理员有权限,不代表应用池账户有权限。Schannel是以应用程序的身份去读私钥的,如果应用池账户是ApplicationPoolIdentity,实际身份是“IIS AppPool\应用池名称”,而私钥文件的ACL里默认可能只有SYSTEM、Administrators和创建者,这就直接导致10013。还有一种情况是证书从旧服务器导出再导入,私钥容器的ACL没有跟着迁移,也会出现同样问题。所以看到这个报错,第一步不是去改TLS版本,而是先确认私钥权限。
1.2 为什么10013不是普通的“连不上”
普通的TLS连接失败,比如协议版本不匹配、密码套件不兼容、证书过期、证书链不完整,通常会在事件日志里看到36870、36874、36875、36876等事件,提示“收到TLS 1.0连接请求,但服务器不支持”或者“证书链不完整”。而36871加上10013,更多是“凭据准备阶段”就失败了,还没走到真正的网络握手。你可以把它类比成:你要进公司大楼,门禁系统先要读取你的工牌芯片,结果读取器没有权限访问芯片数据,于是门禁直接报“内部错误”,而不是“工牌过期”。所以这类问题的排查重心应该放在证书私钥、服务账户、ACL、证书存储权限、Schannel配置上,而不是一上来就抓包看TLS握手。当然,抓包也有用,但如果客户端连Client Hello都没发出去,抓包只能看到TCP连接建立后没有后续,这时候看Schannel日志更直接。另外,10013在Windows不同组件里可能被包装成不同错误码,比如.NET可能抛“CryptographicException: 创建TLS客户端凭据时发生严重错误”,WinHTTP可能返回12175或12175的变体,IIS日志里可能只看到500。因此,定位时要交叉看系统日志、应用日志和CAPI2日志,不能只盯一个地方。
2. 我的排查顺序:从日志到证书权限
2.1 用事件查看器和CAPI2日志抓现场
我遇到这类问题时,第一步永远是打开事件查看器,筛选系统日志里的Schannel来源。命令行走一遍最快:
Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Schannel'} -MaxEvents 100 | Select-Object TimeCreated, Id, LevelDisplayName, Message | Format-List如果看到36871,并且消息里明确写了“内部错误状态为 10013”,基本可以锁定凭据创建失败。接着启用CAPI2日志,路径是“事件查看器 -> 应用程序和服务日志 -> Microsoft -> Windows -> CAPI2 -> Operational”,右键启用日志。启用后重现一次业务请求,再回来看CAPI2事件。CAPI2会记录证书链构建、私钥访问、证书存储读取等细节,经常能看到“无法打开私钥容器”或者“访问被拒绝”的明确提示。这一步非常关键,因为Schannel的10013只是一个结果,CAPI2能告诉你到底是哪张证书、哪个私钥、哪个身份被拒绝了。有些环境里CAPI2日志默认没启用,启用后记得观察完再关闭,避免日志膨胀。如果CAPI2日志里出现“CryptAcquireCertificatePrivateKey failed”,错误码0x8009000B或0x80070005,那就直接跳到私钥权限章节。另外,如果是IIS应用,还可以看IIS日志和HTTPERR日志,但不要只看HTTP状态码,因为TLS凭据失败可能发生在HTTP层之前。我一般会同时开一个PowerShell窗口,循环检测应用池状态和Schannel事件,这样重现时能立刻抓到时间点。
2.2 用certutil和PowerShell确认证书与私钥状态
锁定时间点后,下一步是确认证书和私钥的可用性。先列出本地计算机个人存储里的证书:
Get-ChildItem Cert:\LocalMachine\My | Select-Object Subject, Thumbprint, HasPrivateKey, NotAfter, EnhancedKeyUsageList | Format-Table -AutoSize找到业务使用的证书指纹,然后检查私钥是否可读。以管理员身份运行:
certutil -store My <证书指纹>如果输出里显示“私钥不存在”或者“缺少私钥”,那说明证书导入时没有包含私钥,或者私钥被删了。如果显示有私钥,但应用池账户读不到,certutil以管理员身份可能仍然能读,所以还需要模拟应用池身份测试。可以用PowerShell获取私钥的唯一容器名:
$thumb = "<证书指纹>" $cert = Get-Item "Cert:\LocalMachine\My\$thumb" $cert.HasPrivateKey $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) $rsa.Key.UniqueName拿到UniqueName后,去C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys找对应文件,用icacls查看ACL:
icacls "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\<UniqueName>"如果ACL里没有应用池账户,或者只有管理员和SYSTEM,那10013的根因基本就坐实了。如果是CNG密钥,路径可能在C:\ProgramData\Microsoft\Crypto\Keys,检查方法类似。这里有个细节:不要直接给Everyone读权限,最小权限原则是只给需要的服务账户读权限。另外,如果证书是“不可导出”的,某些修复操作会受限,必要时重新导入一份带私钥且允许导出的证书,再重新授权。
3. 高频根因逐项击破
3.1 私钥文件ACL没有放行应用池身份
这是最常见的根因,没有之一。IIS应用池默认使用ApplicationPoolIdentity,实际账户名是“IIS AppPool\应用池名称”。比如应用池叫MyAppPool,账户就是IIS AppPool\MyAppPool。如果应用以NETWORK SERVICE运行,账户就是NETWORK SERVICE。如果应用以自定义域账户运行,就是那个域账户。修复时打开证书管理单元:
certlm.msc找到“个人 -> 证书”,右键目标证书 -> 所有任务 -> 管理私钥。在弹出的权限窗口里添加对应的应用池账户,至少给“读取”权限。如果“管理私钥”菜单灰掉,说明证书没有私钥或者当前用户没有权限,需要先用管理员身份操作。也可以用命令行:
icacls "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\<UniqueName>" /grant "IIS AppPool\MyAppPool":R授予后重启应用池:
Restart-WebAppPool -Name "MyAppPool"注意,有些环境里应用池账户名需要写成IIS AppPool\MyAppPool,不能只写MyAppPool,否则会提示找不到账户。如果应用池是“无托管代码”或“经典模式”,身份可能不同。还有一点,如果证书私钥文件所在的父目录ACL有问题,比如MachineKeys目录本身没有继承权限,也可能导致读取失败。我通常会检查父目录的ACL是否包含SYSTEM和Administrators完全控制,但不建议随意改父目录,只改具体私钥文件即可。
3.2 Schannel协议与密码套件配置被改乱
有时候10013并不是私钥权限,而是Schannel本身的配置被安全加固脚本改乱了。比如为了满足某些扫描要求,有人把TLS 1.0/1.1全部禁用,同时又把几乎所有RSA密码套件禁用,只留下ECDHE套件,但证书私钥是RSA的,Schannel在创建客户端凭据时可能找不到可用的套件组合,结果报内部错误。检查注册表:
reg query "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols" /s确认TLS 1.2的Client和Server子键下,Enabled为1,DisabledByDefault为0。如果没有这些键,系统默认可能还是启用的。如果需要显式启用:
reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" /v Enabled /t REG_DWORD /d 1 /f reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" /v DisabledByDefault /t REG_DWORD /d 0 /f reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server" /v Enabled /t REG_DWORD /d 1 /f reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server" /v DisabledByDefault /t REG_DWORD /d 0 /f改完注册表需要重启服务器或至少重启相关服务,Schannel配置不是热加载的。密码套件可以用Get-TlsCipherSuite查看,如果发现3DES相关套件被禁用,那是为了应对CVE-2016-2183这类信息泄露漏洞,属于正常安全加固,但不要禁用所有RSA套件。如果确实需要禁用弱套件,建议保留TLS_RSA_WITH_AES_128_CBC_SHA或TLS_RSA_WITH_AES_256_CBC_SHA等强套件,确保RSA证书还有可用组合。修改密码套件顺序最好通过组策略“SSL密码套件顺序”来做,不要手改注册表后忘记备份。
3.3 证书链、系统时间和根证书更新
虽然10013主要是权限,但证书链问题也可能间接导致凭据创建失败。比如中间证书缺失,Schannel在构建客户端凭据时无法验证证书链,可能返回内部错误。用certutil -verify检查:
certutil -verify -urlfetch "C:\path\to\cert.cer"如果提示“证书链不完整”或“无法获取颁发者”,需要把中间证书导入“中间证书颁发机构”存储。根证书更新也很重要,尤其是离线环境或长期未更新的服务器。可以运行:
certutil -generateSSTFromWU roots.sst然后手动导入,或者直接依赖Windows Update自动更新。系统时间偏差过大也会导致证书验证失败,用w32tm /resync同步时间。我遇到过一台虚拟机从快照恢复后时间倒退,导致所有TLS客户端请求失败,事件日志里既有36871也有证书过期提示,同步时间后立刻恢复。所以排查时不要忽略时间同步这个看似低级的点。另外,如果证书本身已过期,或者证书的“客户端身份验证”EKU缺失,也会导致Schannel拒绝使用该凭据。检查证书的增强密钥用法,确保包含“客户端身份验证”或“任何目的”。
3.4 组策略、FIPS与应用池账户配置
企业环境里,组策略可能强制启用FIPS兼容算法,这会影响Schannel的密码套件选择。检查:
reg query "HKLM\SYSTEM\CurrentControlSet\Control\Lsa\FIPSAlgorithmPolicy" /v Enabled如果Enabled为1,而你的证书或应用依赖非FIPS算法,就可能出现问题。可以尝试临时关闭FIPS测试,但生产环境要评估合规要求。另一个常见问题是应用池的“加载用户配置文件”选项。如果证书私钥被放在了用户配置文件的密钥容器里,而应用池没有加载用户配置文件,就读取不到。在IIS管理器中,应用池高级设置里把“加载用户配置文件”设为True,然后重启应用池。还有,如果应用是以服务方式运行,服务的登录身份可能不是你以为的那个账户。用sc qc 服务名查看服务账户,或者用任务管理器查看进程身份。有些服务用LocalSystem运行,但证书私钥ACL里没有SYSTEM读权限,也会10013。不要假设,一定要实际确认进程身份。
4. 实操修复:从备份到验证的完整记录
4.1 备份证书与私钥,避免越修越乱
动手之前先备份。证书和私钥一旦弄丢,重新签发和部署的成本很高。在证书管理单元里导出证书,选择“是,导出私钥”,设置一个临时密码,保存为PFX文件。如果私钥标记为不可导出,可以先尝试用certutil -exportPFX命令:
certutil -exportPFX -p "临时密码" My <证书指纹> C:\backup\cert.pfx如果提示不可导出,可能需要重新导入证书并勾选“允许导出私钥”,或者联系证书颁发机构重新签发。备份完PFX后,再导出证书的ACL信息?ACL不能直接导出,但可以记录当前的权限列表。可以用icacls把私钥文件权限保存到文本:
icacls "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\<UniqueName>" > C:\backup\key-acl.txt同时记录注册表中Schannel相关的键值:
reg export "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" C:\backup\schannel.reg这样即使改错了,也能快速回滚。我见过有人直接删除MachineKeys下的私钥文件想让它重建,结果证书彻底无法使用,业务停了几个小时。所以备份是底线。
4.2 重置私钥权限的三种可靠方法
第一种是图形界面:certlm.msc-> 个人 -> 证书 -> 所有任务 -> 管理私钥 -> 添加账户 -> 输入IIS AppPool\MyAppPool-> 检查名称 -> 确定 -> 勾选“读取”。这种方法最直观,适合不熟悉命令行的朋友。第二种是icacls命令行,前面已经给出,优点是可以批量处理,也可以写成脚本。第三种是certutil -repairstore,当私钥容器损坏或ACL异常时,可以尝试:
certutil -repairstore My <证书指纹>这个命令会尝试修复证书与私钥的关联,但要注意,它有时会重新生成私钥容器,导致原有权限丢失,所以执行后要重新检查并授予应用池读权限。如果环境里有多个应用池共用同一张证书,需要给每个应用池账户都授予读权限,或者考虑用同一个服务账户运行这些应用池。不要图省事给Everyone读权限,私钥泄露风险很高。如果使用负载均衡,每台服务器都要做同样的权限修复,否则请求转到未修复的节点时又会失败。
4.3 修复Schannel注册表并重启相关服务
如果确认是Schannel协议配置问题,先导出备份,再按需修改。通常保留TLS 1.2和TLS 1.3即可,TLS 1.0/1.1如果业务允许可以禁用,但不要同时把加密套件限制得过死。修改后重启服务器是最稳妥的,如果业务不允许重启,至少重启以下服务:
Restart-Service -Name "HTTP" -Force Restart-Service -Name "WinHTTP Web Proxy Auto-Discovery Service" -Force Restart-WebAppPool -Name "MyAppPool"如果是Windows服务调用TLS,重启对应服务。注意,Schannel没有独立的服务,它由内核和LSASS等组件承载,所以注册表修改后最好重启。另外,如果启用了CAPI2日志,记得修复后关闭,避免日志占满磁盘。有些安全加固脚本会写入“DisabledByDefault=1”,这会把TLS 1.2也默认禁用,导致大量TLS失败,要特别检查。对于CVE-2016-2183这类扫描项,正确的做法是禁用3DES和RC4等弱套件,而不是禁用整个协议族。可以使用:
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_3DES_EDE_CBC_SHA" Disable-TlsCipherSuite -Name "TLS_RSA_WITH_RC4_128_SHA"执行后再次确认至少还有TLS_RSA_WITH_AES_128_CBC_SHA或TLS_RSA_WITH_AES_256_CBC_SHA可用。
4.4 验证TLS客户端凭据是否恢复正常
修复完成后,不要只看事件日志不再报错,最好主动发起一次TLS客户端请求。可以用PowerShell:
$url = "https://example.com" try { $req = [System.Net.HttpWebRequest]::Create($url) $req.Method = "HEAD" $resp = $req.GetResponse() Write-Host "成功,状态码:" $resp.StatusCode $resp.Close() } catch { Write-Host "失败:" $_.Exception.Message }如果是IIS应用,直接访问业务页面,观察是否恢复。同时在事件查看器里筛选Schannel,确认没有新的36871。还可以用Test-NetConnection测试端口连通性,但端口通不代表TLS凭据创建成功。更彻底的验证是使用SslStream写一个小脚本,指定客户端证书:
$cert = Get-Item "Cert:\LocalMachine\My\<证书指纹>" $client = New-Object System.Net.Sockets.TcpClient("example.com", 443) $ssl = New-Object System.Net.Security.SslStream($client.GetStream(), $false) $ssl.AuthenticateAsClient("example.com", $cert, [System.Security.Authentication.SslProtocols]::Tls12, $false) Write-Host "TLS握手成功,协议:" $ssl.SslProtocol $ssl.Close() $client.Close()如果这个脚本以应用池身份运行也能成功,那基本就没问题了。注意,测试时不要使用真实敏感站点,用公开测试站点即可。
5. 常见问题速查表与避坑心得
5.1 现象、可能原因、处理动作对照表
| 现象 | 可能原因 | 处理动作 |
|---|---|---|
| Schannel事件36871,内部状态10013 | 私钥ACL缺少应用池读权限 | 用certlm.msc或icacls授予IIS AppPool\...读权限 |
| 仅IIS应用报错,其他程序正常 | 应用池身份与应用池名称不匹配 | 确认应用池实际账户,重新授权 |
| 重启后短暂恢复,随后又失败 | 组策略刷新覆盖配置,或证书服务重新生成权限 | 检查组策略结果集,确认私钥ACL是否被重置 |
| 系统时间偏差大后出现 | 证书链验证失败 | 同步时间,更新根证书 |
| 禁用TLS 1.0/1.1后出现 | 协议或密码套件配置不兼容 | 确保TLS 1.2启用,保留强RSA套件 |
| 多个应用池共用证书,部分失败 | 只给了一个应用池权限 | 给所有使用该证书的应用池账户授权 |
| 证书管理单元里“管理私钥”灰色 | 证书无私钥或当前用户无权限 | 用管理员运行certlm.msc,或重新导入带私钥证书 |
| CAPI2日志显示0x80070005 | 访问被拒绝 | 按私钥权限问题处理 |
5.2 我踩过的坑和反直觉结论
第一个坑:以为给IIS_IUSRS就万事大吉。实际上ApplicationPoolIdentity不是IIS_IUSRS组的成员,它是虚拟账户,必须显式添加IIS AppPool\应用池名称。第二个坑:只重启IIS,没重启应用池。IIS重启不会重新加载应用池身份,权限变更后必须回收应用池。第三个坑:用管理员账户测试成功就以为修好了。管理员权限太大,必须用应用池身份测试,或者直接看业务请求是否恢复。第四个坑:盲目运行certutil -repairstore。这个命令有时会重置私钥容器,导致原本正常的证书也需要重新授权,所以先备份再操作。第五个坑:忽略证书链。我曾遇到一个环境,私钥权限完全正确,但中间证书缺失,Schannel偶尔报10013,导入中间证书后彻底稳定。第六个坑:安全加固脚本一刀切。有些脚本把TLS 1.0/1.1全禁,同时把3DES禁用,这本身没问题,但把所有RSA套件也禁了,而证书是RSA的,结果客户端凭据创建失败。正确的做法是禁用弱算法,保留强RSA套件,或者改用ECC证书。第七个坑:在负载均衡环境只修一台。请求轮询到其他节点时继续报错,所以要么统一修复,要么临时把故障节点下线。
6. 别混淆:10013在不同场景下的面孔
6.1 与socket 10013、bind 80端口失败的区别
os error 10013经常出现在socket编程里,意思是“访问套接字的方式被权限禁止”。比如bind() to 0.0.0.0:80 failed (10013: an attempt was made to access a socket in a way forbidden by its access permissions),这通常是因为非管理员账户尝试绑定80端口,或者端口被系统保留、被其他程序占用。解决方法是换端口、以管理员运行、或者用netsh http show urlacl检查URL保留。而TLS客户端凭据的10013是Schannel内部状态,跟socket绑定无关。两者都带10013,但一个在传输层,一个在安全凭据层。排查时先看报错来源:如果是Schannel事件,就走证书私钥权限;如果是应用日志里写bind失败,就走端口权限和占用排查。不要把两者混为一谈,否则会浪费很多时间。codex桌面版error 10013可能就是应用包装了底层socket错误,也可能是证书相关,需要看具体日志。判断标准是看错误上下文里有没有“TLS客户端凭据”字样。
6.2 与0x80070643、浏览器TLS弃用报错的区分
0x80070643-安装时发生严重错误是Windows更新或安装程序常见的错误码,通常跟.NET Framework、Visual C++运行库、Windows Installer有关,和TLS凭据没有直接关系。如果你在安装某个软件时同时看到0x80070643和TLS错误,要分开处理:先解决安装问题,再解决TLS问题。浏览器报错“无法安全地连接到此页面 这可能是因为该站点使用过期的或不安全的 TLS 安全设置”或者“火狐报错 该网站使用了已弃用的 TLS 版本。请升级到 tls 1.2 或 1.3。”,这是浏览器作为客户端发现服务器只支持TLS 1.0/1.1,或者证书过期、证书链不完整。这类问题需要改服务器配置,启用TLS 1.2/1.3,更新证书,而不是去修客户端私钥权限。还有the tls certificates for the following protocols have expired,通常指某些协议使用的证书已过期,需要检查证书有效期。把这几类错误分清,能避免在错误方向上折腾。我的习惯是先看错误码前缀:0x8007开头多是Win32安装类错误,36871是Schannel事件,浏览器提示是UI层包装,三者的处理路径完全不同。
6.3 嵌入式TLS与PSK场景的简单对照
stm32 mqtt tls加密通信这类场景通常用mbedTLS或类似库,客户端凭据是编译进固件的证书和私钥数组,不存在Windows ACL问题。如果握手失败,优先检查证书格式、时间有效性、根证书、TLS版本、密码套件,以及设备时间是否同步。tls + psk是预共享密钥模式,不使用证书凭据,所以一般不会出现“创建TLS客户端凭据”这种错误。如果PSK和证书混合使用,配置错误可能导致握手失败,但错误信息通常不会映射成Windows Schannel的10013。Wireshark TLS解密在排查这些场景时很有用,但前提是你有私钥并且密钥交换算法允许解密,比如RSA密钥交换,ECDHE前向保密则无法直接用私钥解密。不过对于Windows Schannel的10013,抓包往往看不到Client Hello,因为凭据还没创建成功,所以优先看系统日志和CAPI2日志。最后再分享一个小技巧:把Get-WinEvent和icacls检查写成一段脚本,每次修复后跑一遍,确认事件日志没有新增36871,同时确认私钥ACL包含正确的应用池账户。这个习惯让我在后续几次类似故障里,从接到报错到恢复业务基本控制在二十分钟以内。