编程语言

OpenSSL::SSL::SSLError certificate verify failed(Ruby)

作者: · 最后核实

报错信息OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 state=error: certificate verify failed (unable to get local issuer certificate)

OpenSSL::SSL::SSLError ... certificate verify failed 的意思是:Ruby 的 OpenSSL 连上了服务器,但没能用它加载的 CA 证书验证服务器的证书链。 通常是 Ruby 去找的 CA 文件不存在或者过旧,或者对方服务器没有发送中间证书。

快速修复

多数情况让 Ruby 用上一个有效的 CA 证书包就能修好。先看你的 Ruby 用的是哪个 OpenSSL、去哪里找 CA 证书:

ruby -ropenssl -e 'puts OpenSSL::OPENSSL_LIBRARY_VERSION, OpenSSL::X509::DEFAULT_CERT_FILE, OpenSSL::X509::DEFAULT_CERT_DIR'
ls -l "$(ruby -ropenssl -e 'print OpenSSL::X509::DEFAULT_CERT_FILE')"

文件不存在时,让 Ruby 使用一个有效的证书包:

export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt   # Debian/Ubuntu 路径

gem 和 bundle 报错时,运行 bundle doctor ssl(较新的 Bundler)和 gem update --system。如果只有某一个域名报错,用 SSL 检测工具检查它的证书链。

原因

Ruby 找不到 CA 文件或 CA 过旧最常见,其次是对方服务器没发中间证书,下面共列出 5 种原因。

1. Ruby 的 OpenSSL 找不到 CA 文件

用 rbenv/ruby-build、asdf、rvm 编译的 Ruby 会链接某个特定的 OpenSSL(macOS 上通常是 Homebrew 的)。这个 OpenSSL 没有证书文件,或者升级时被删掉了,所有校验都会失败。

  • 确认: DEFAULT_CERT_FILE 指向的路径不存在或是空文件。
  • 修复: 安装或更新该 OpenSSL 对应的 CA 证书包,设置 SSL_CERT_FILE,或者重新安装这个 Ruby 版本,让它链接到当前的 OpenSSL。

2. CA 证书或 RubyGems 版本过旧

老系统和旧版 RubyGems 缺少较新的根证书。

  • 确认: 报错的网站所用根证书比你的系统或 RubyGems 版本更新。
  • 修复: 更新系统 ca-certificates 包,执行 gem update --system 和 bundle update --bundler。CentOS 7 这类已停止维护的系统,证书包和 OpenSSL 都很旧,长期看应升级操作系统。

3. 系统时间不对

RubyGems 指南把系统时间错误列为两大原因之一:时间偏到过去或未来,有效证书也会显示为已过期或尚未生效。

  • 修复: 校正时间并开启 NTP 自动同步。国内服务器可以用阿里云、腾讯云提供的 NTP 服务器。

4. 对方服务器证书链不完整

  • 确认: openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts 只列出一张证书。
  • 修复: 需要对方服务器发送中间证书,见缺少中间证书和证书链补全工具。

5. 公司代理或内网 CA

不在公开证书包里的根证书需要显式添加:

require "net/http"
require "openssl"

store = OpenSSL::X509::Store.new
store.set_default_paths
store.add_file("/etc/ssl/certs/company-root.pem")

http = Net::HTTP.new("intranet.example.com", 443)
http.use_ssl = true
http.cert_store = store
puts http.get("/").code

如果整台机器都要信任,就把根证书加进系统信任库。报错文字是 self-signed certificate in certificate chain 时,见那一页。

各环境修法

不管哪个平台,修法都是更新 OpenSSL 和 CA 证书包,让 Ruby 读到它;私有根证书加进系统信任库或用 SSL_CERT_FILE 指定。

macOS + Homebrew + rbenv/asdf: 执行 brew upgrade openssl@3 ca-certificates,再用 ruby-build 重新安装受影响的 Ruby 版本,让它链接当前的 OpenSSL。临时办法是 export SSL_CERT_FILE="$(brew --prefix)/etc/ca-certificates/cert.pem"。

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 / Alibaba Cloud Linux: 根证书放进 /etc/pki/ca-trust/source/anchors/,再执行 update-ca-trust。

Windows(RubyInstaller): 用上面的命令看 Ruby 去哪里找证书,再用 SSL_CERT_FILE 指向一个包含公司根证书的证书包。

Docker: 镜像里安装 ca-certificates,并在 bundle install 之前把私有根证书复制进去。

国内镜像源: 使用 Ruby China 镜像时,地址保持 https://:bundle config mirror.https://rubygems.org https://gems.ruby-china.com。换源解决的是速度问题,证书错误仍要按上面的方法处理。

Faraday / HTTParty: 都可以指定 CA 文件或证书库(Faraday 用 ssl: { ca_file: ... },HTTParty 用 ssl_ca_file:)。

不要这样做

关闭校验能让报错消失,但任何人伪造的证书都会被接受。

  • http.verify_mode = OpenSSL::SSL::VERIFY_NONE。 接受任何人的任何证书,账号密码和接口数据都可能被截获。
  • 重定义 OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE。 这段老代码会让进程里所有库都不再校验证书。
  • 在 .gemrc 里写 :ssl_verify_mode: 0,或把 gem 源改成 http://。 gem 是你要执行的代码,不经认证地下载等于给篡改敞开大门。

常见问题

gem install 或 bundle install 报 certificate verify failed,先做什么?

Bundler 支持的话先运行 bundle doctor ssl,然后按 RubyGems 指南依次处理:升级 RubyGems 和 Bundler(gem update --system)、检查系统时间、更新系统 CA 证书。访问 rubygems.org 或 gems.ruby-china.com 失败,大多数靠这三步就能解决。

同一台机器上 curl 正常,Ruby 却报错,为什么?

Ruby 用的是编译时链接的那个 OpenSSL,它和 curl 命令可能不是同一份,默认 CA 路径也不同。对比 ruby -ropenssl -e 'puts OpenSSL::X509::DEFAULT_CERT_FILE' 和 curl -v 输出里的 CAfile:,Ruby 的文件不存在时设置 SSL_CERT_FILE。

升级 Homebrew 或 macOS 之后才开始报错,怎么回事?

ruby-build、rvm 编译的 Ruby 会链接某一个具体的 OpenSSL 安装。这个 OpenSSL 或它的证书文件被移动、删除后,Ruby 就加载不到根证书了。重新安装该 Ruby 版本(让它链接当前的 OpenSSL),或者用 SSL_CERT_FILE 指向有效的证书包。

只想让某一个 HTTP 客户端信任公司根证书,怎么写?

建一个包含默认根证书和公司根证书的证书库传给客户端:store = OpenSSL::X509::Store.new; store.set_default_paths; store.add_file("/path/company-root.pem"),Net::HTTP 用 http.cert_store = store,Faraday 用 ssl: { cert_store: store }。

资料来源

  1. RubyGems guides: How to troubleshoot RubyGems and Bundler TLS/SSL issues
  2. Ruby docs: OpenSSL
  3. OpenSSL 3.0: X509_STORE_CTX_get_error (verify error codes)
  4. curl: CA certificates extracted from Mozilla