命令行工具

curl: (60) SSL certificate problem 报错原因与正确修法

作者: · 最后核实

报错信息curl: (60) SSL certificate problem: unable to get local issuer certificate

curl: (60) SSL certificate problem 的意思是:curl 连上了服务器,但没法从服务器证书一路验证到它信任的根证书。 大多数情况是服务器没发送中间证书;其余情况是本机 CA 证书包缺失、过旧,或者缺少公司内网的私有根证书。

快速修复

先分清是服务器证书链不完整,还是本机信任库有问题,两者修法不同。在一台干净的机器上执行:

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
  • 输出里只有一张证书(只有 depth=0),最后是 Verify return code: 21 (unable to verify the first certificate) 或 20 (unable to get local issuer certificate):服务器证书链不完整,到服务器上装完整证书链,见缺少中间证书。
  • 服务器发了完整的链,其他机器访问也正常:是你这台机器的信任库有问题。更新 CA 证书包,或者让 curl 用正确的根证书:
curl --cacert /path/to/company-root.pem https://example.com
# 当前 shell 里所有 curl 都生效
export CURL_CA_BUNDLE=/path/to/ca-bundle.pem

也可以把域名输入 SSL 检测工具,直接看服务器实际发出的证书链。

原因

服务器没发中间证书是最常见的原因,其次是本机 CA 证书包缺失或过旧。

1. 服务器没发中间证书

这是最常见的原因。服务器应该发送站点证书和全部中间证书(根证书可以不发)。浏览器会帮你掩盖这个错误,curl 不会。

  • 确认: openssl s_client -showcerts 只列出一张证书,它的 issuer 是某个中间 CA 而不是根证书。
  • 修复: Nginx 的 ssl_certificate 要指向 fullchain.pem(站点证书在前、中间证书在后),不要用 cert.pem;Apache 2.4.8 以上把完整链写进 SSLCertificateFile。阿里云、腾讯云下载的证书包里一般已有 bundle 或 chain 文件,按服务器类型选对文件。手上只有站点证书时,可以用证书链补全工具生成完整链。

2. 本机 CA 证书包缺失或过旧

精简版 Docker 镜像、老旧的 CentOS 6/7 系统经常没装或没更新 CA 证书包,新根证书签发的网站就会验证失败。

  • 确认: curl -v https://example.com 2>&1 | grep -i cafile 显示的文件不存在,或者里面没有服务器所用的根证书。
  • 修复: 安装或更新系统的 ca-certificates 包;也可以从 curl.se 下载 Mozilla 证书包,用 --cacert 指定。

3. 企业代理、上网行为管理或杀毒软件做了 HTTPS 解密

很多公司的上网行为管理、安全网关会解密 HTTPS 再用自己的根证书重新签名,部分杀毒软件的“HTTPS 扫描”也是这样。系统证书库里装了这个根证书,但 curl 用的证书包里没有,于是报 60。

  • 确认: openssl s_client 输出里的 issuer 是代理厂商或公司名,而不是公开 CA。
  • 修复: 向 IT 部门要这张根证书,加入系统信任库(见下文),或用 --cacert 指定。

4. 自签名证书或内部私有 CA

内网服务、测试环境常用自签名证书或公司自建 CA,这时报错文字通常是 self-signed certificate 或 self-signed certificate in certificate chain。

5. 域名不匹配或证书过期

如果报错里有 subject name does not match 或 certificate has expired,说明证书链本身验证通过了,但证书的域名或有效期不对。修改证书的 SAN 列表或重新签发、续期。

各环境修法

修法只有两种:更新系统 CA 证书包,或者把私有根证书追加进系统信任库;各环境只是命令不同。

Debian / Ubuntu

sudo apt-get install --reinstall ca-certificates
sudo cp company-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

CentOS / RHEL / Rocky / 阿里云 Alibaba Cloud Linux

sudo yum reinstall -y ca-certificates
sudo cp company-root.pem /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

Alpine 和 Docker 镜像

RUN apk add --no-cache ca-certificates
COPY company-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates

国内构建时如果 apk add 很慢,可以先把 Alpine 源换成阿里云或清华等国内镜像,但不要因此跳过 ca-certificates 的安装。Debian 系镜像用 apt-get install -y ca-certificates。

macOS: 系统自带的 curl 用钥匙串。在“钥匙串访问”里把根证书导入“系统”钥匙串并设为“始终信任”。Homebrew 安装的 curl 用自己的证书包,用 curl -V 和 curl -v 输出里的 CAfile: 确认。

Windows: Windows 10/11 自带的 curl.exe 使用 Schannel 和 Windows 证书库,在管理员命令行执行 certutil -addstore -f Root company-root.cer 导入根证书即可。

PHP、Git 等基于 libcurl 的程序会遇到同样的问题,见 PHP cURL error 60 和 Git SSL certificate problem。

不要这样做

-k 一类的做法会关闭证书校验,换证书包则可能引入来历不明的信任根。

  • 在脚本里用 curl -k / --insecure。 它会关闭全部证书校验,连域名校验也一起关掉。中间人可以直接读到 API 密钥、篡改下载的安装包。
  • 往 ~/.curlrc 里写 insecure。 这会让该账号以后所有 curl 调用悄悄失去校验,问题被掩盖很久都不会被发现。
  • 用论坛、网盘里来路不明的证书包替换系统证书包。 用系统包管理器或 curl.se 发布的证书包,私有根证书是“追加”进去,而不是“替换”掉原有的。

常见问题

curl 退出码 60 具体是什么意思?

60 对应 CURLE_PEER_FAILED_VERIFICATION:TLS 握手本身成功了,但服务器证书或证书链没能用 curl 信任的 CA 验证通过。冒号后面的文字(如 unable to get local issuer certificate、self-signed certificate)是 OpenSSL 给出的具体原因。

为什么浏览器能打开,curl 却报 60?

Chrome 等浏览器会自动下载缺失的中间证书,或者已经从别的网站缓存了它;curl 两样都不做。所以服务器只发站点证书时,浏览器正常、curl 报错。用证书链补全工具检查后在服务器上修复。

什么时候可以用 curl -k?

只适合在自己控制的主机上、已经清楚原因时临时手动测试。脚本、CI、定时任务里用 -k 等于关掉全部证书校验,同一网络路径上的任何人都能冒充服务器,读取或篡改数据。

curl 默认从哪里读 CA 证书?

取决于编译时用的 TLS 库。OpenSSL 版本一般用编译时写死的路径,如 Debian/Ubuntu 的 /etc/ssl/certs/ca-certificates.crt、CentOS/RHEL 的 /etc/pki/tls/certs/ca-bundle.crt;Windows 自带的 curl.exe 和 macOS 系统 curl 用系统证书库。执行 curl -v https://example.com,看输出里的 CAfile: 一行即可确认。

宝塔面板上的网站 curl 报 60 怎么办?

先确认部署的是完整证书链。在宝塔的 SSL 设置里,“证书(PEM格式)”一栏要粘贴站点证书加中间证书(CA 下载包里的 fullchain 或 bundle 文件),只贴站点证书就会让 curl、手机 App 和部分安卓浏览器报错。

资料来源

  1. curl: SSL Certificate Verification
  2. libcurl error codes
  3. curl: CA certificates extracted from Mozilla
  4. curl man page