【Bug已解决】openclaw TLS certificate validation failed — OpenClaw TLS 证书验证失败解决方案
【Bug已解决】openclaw: TLS certificate validation failed / UNABLE_TO_VERIFY_LEAF_SIGNATURE — OpenClaw TLS证书验证失败解决方案1. 问题描述在使用 OpenClaw 通过 HTTPS 连接外部服务如 API 调用、Git 操作、包下载时系统报出 TLS 证书验证失败错误# TLS证书验证失败 - 标准报错 $ openclaw 调用外部API Error: TLS certificate validation failed UNABLE_TO_VERIFY_LEAF_SIGNATURE unable to verify the first certificate # 自签名证书不受信任 $ openclaw 连接内部Git服务 Error: SELF_SIGNED_CERT_IN_CHAIN self signed certificate in certificate chain Hostname/IP does not match certificates altnames # 证书过期 $ openclaw 下载依赖包 Error: CERT_HAS_EXPIRED certificate has expired NotAfter: 2024-01-01T00:00:00 # 证书链不完整 $ openclaw 访问企业内网服务 Error: UNABLE_TO_GET_ISSUER_CERT unable to get issuer certificate Certificate chain incomplete这个问题在以下场景中特别常见企业内网使用自签名证书中间证书缺失导致证书链不完整系统时间不正确导致证书验证失败企业安全网关拦截 HTTPS 流量证书已过期未及时更新Node.js 不使用系统证书存储2. 原因分析OpenClaw发起HTTPS请求 ↓ TLS握手 ←──── 服务器返回证书链 ↓ 验证证书 ←──── 检查CA信任链 ↓ 验证失败 ←──── 自签名/过期/链不完整 ↓ 抛出 TLS证书验证错误原因分类具体表现占比自签名证书SELF_SIGNED约 30%证书链不完整缺中间证书约 25%安全网关证书替换约 20%证书过期CERT_HAS_EXPIRED约 10%系统时间错误时间偏差约 8%Node.js CA 存储不读系统证书约 7%深层原理TLS 证书验证是 HTTPS 安全的核心机制。当客户端连接服务器时服务器返回证书链服务器证书 → 中间证书 → 根证书。客户端需要验证整条信任链从服务器证书开始逐级向上验证到根证书颁发机构CA最终到达客户端受信任的根证书列表。Node.js 使用内置的 CA 证书列表由 Mozilla 维护的 CA 证书包而不是操作系统的证书存储。这意味着即使系统信任了某个自签名证书Node.js 仍然会拒绝连接。企业环境中IT 部门通常在系统中安装自签名的根证书用于安全网关 TLS 检查但 Node.js 不会自动使用它。3. 解决方案方案一配置 Node.js 使用系统证书最推荐# 方法1: 使用 NODE_EXTRA_CA_CERTS 环境变量 # 将企业根证书添加到 Node.js 信任链 export NODE_EXTRA_CA_CERTS/etc/ssl/certs/company-root-ca.pem openclaw 连接内部服务 # 方法2: 合并多个证书 cat /etc/ssl/certs/company-root-ca.pem ~/.openclaw/ca-bundle.pem cat /etc/ssl/certs/company-intermediate-ca.pem ~/.openclaw/ca-bundle.pem export NODE_EXTRA_CA_CERTS$HOME/.openclaw/ca-bundle.pem openclaw 任务 # 永久设置 echo export NODE_EXTRA_CA_CERTS$HOME/.openclaw/ca-bundle.pem ~/.zshrc source ~/.zshrc # 方法3: 在 OpenClaw 配置中设置 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[tls] { caBundlePath: /etc/ssl/certs/company-root-ca.pem, rejectUnauthorized: True, # 保持验证安全 useSystemCerts: True # 尝试使用系统证书 } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(TLS 配置已更新: 使用企业根证书) # 验证证书是否有效 openssl x509 -in /etc/ssl/certs/company-root-ca.pem -text -noout | head -20方案二提取并安装缺失的中间证书# 查看服务器的完整证书链 echo | openssl s_client -connect internal-api.company.com:443 -showcerts 2/dev/null | \ grep -E subject|issuer|depth # 提取服务器返回的所有证书 echo | openssl s_client -connect internal-api.company.com:443 -showcerts 2/dev/null | \ awk /-----BEGIN CERTIFICATE-----/{i; out/tmp/cert_i.pem} {print out} # 查看提取的证书 ls -la /tmp/cert_*.pem # 验证每个证书 for cert in /tmp/cert_*.pem; do echo $cert openssl x509 -in $cert -subject -issuer -dates -noout echo done # 将中间证书添加到信任链 # 通常 cert_1 是服务器证书, cert_2 是中间证书 cp /tmp/cert_2.pem ~/.openclaw/intermediate-ca.pem export NODE_EXTRA_CA_CERTS$HOME/.openclaw/intermediate-ca.pem # 验证修复 echo | openssl s_client -connect internal-api.company.com:443 -CAfile ~/.openclaw/intermediate-ca.pem 2/dev/null | \ grep Verify return code # 应输出: Verify return code: 0 (ok)方案三临时禁用 TLS 验证仅开发环境# ⚠️ 警告: 仅用于开发/测试环境生产环境不推荐 # 方法1: 设置环境变量 export NODE_TLS_REJECT_UNAUTHORIZED0 openclaw 连接服务 # 方法2: 在 OpenClaw 配置中 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[tls] { rejectUnauthorized: False # ⚠️ 禁用证书验证 } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(⚠️ TLS验证已禁用仅开发环境) # 方法3: 使用 --insecure 标志 openclaw --insecure 连接服务 # 确保只在开发环境禁用 if [ $NODE_ENV development ]; then export NODE_TLS_REJECT_UNAUTHORIZED0 echo ⚠️ 开发环境: TLS验证已禁用 fi方案四处理企业安全网关证书替换# 企业安全网关可能拦截 HTTPS 流量并重新签名 # 查找安全网关的根证书 # macOS: 在钥匙串中查找 security find-certificate -a -p /Library/Keychains/System.keychain /tmp/gateway-ca.pem # Linux: 在系统证书目录查找 ls /etc/ssl/certs/ | grep -i gateway ls /usr/local/share/ca-certificates/ # 将安全网关证书添加到 Node.js 信任链 export NODE_EXTRA_CA_CERTS/tmp/gateway-ca.pem openclaw 任务 # 配置 OpenClaw 使用安全网关证书 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[tls] { caBundlePath: /tmp/gateway-ca.pem, rejectUnauthorized: True } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(安全网关证书已配置) 方案五修复系统时间导致的证书验证失败# 检查系统时间 date # 如果时间偏差过大证书验证会失败 # 检查时间同步状态 timedatectl status # Linux # 或 sudo sntp -v time.apple.com # macOS # 同步时间 # Linux sudo timedatectl set-ntp true # macOS sudo sntp -sS time.apple.com # 手动设置时间 sudo date -s 2024-07-07 10:30:00 # Linux sudo date 071030002024 # macOS: MMDDhhmmYYYY # Docker 容器中时间不同步 docker run --rm -v /etc/localtime:/etc/localtime:ro node:18 date # 验证修复 date # 确认时间正确 openclaw 连接服务方案六创建统一的证书管理工具# 创建证书管理工具 import ssl import os import subprocess import json class CertificateManager: TLS 证书管理工具 staticmethod def get_cert_info(hostname, port443): 获取服务器证书信息 context ssl.create_default_context() context.check_hostname False context.verify_mode ssl.CERT_NONE try: with ssl.create_connection((hostname, port)) as sock: with context.wrap_socket(sock, server_hostnamehostname) as ssock: cert ssock.getpeercert(True) cert_obj ssl.DER_cert_to_PEM_cert(cert) # 解析证书信息 result subprocess.run( [openssl, x509, -noout, -subject, -issuer, -dates, -fingerprint], inputcert_obj, capture_outputTrue, textTrue ) return result.stdout except Exception as e: return f获取证书失败: {e} staticmethod def download_cert_chain(hostname, port443, output_dir.openclaw/certs): 下载服务器完整证书链 os.makedirs(output_dir, exist_okTrue) result subprocess.run( [openssl, s_client, -connect, f{hostname}:{port}, -showcerts], input, capture_outputTrue, textTrue, timeout10 ) output result.stdout cert_count 0 current_cert [] in_cert False for line in output.split(\n): if BEGIN CERTIFICATE in line: in_cert True current_cert [line] elif END CERTIFICATE in line: current_cert.append(line) cert_count 1 cert_path os.path.join(output_dir, f{hostname}_cert_{cert_count}.pem) with open(cert_path, w) as f: f.write(\n.join(current_cert)) in_cert False elif in_cert: current_cert.append(line) print(f下载了 {cert_count} 个证书) # 合并所有证书为 CA bundle bundle_path os.path.join(output_dir, f{hostname}_ca_bundle.pem) with open(bundle_path, w) as f: for i in range(1, cert_count 1): cert_path os.path.join(output_dir, f{hostname}_cert_{i}.pem) with open(cert_path, r) as cf: f.write(cf.read()) f.write(\n) print(fCA bundle 已生成: {bundle_path}) return bundle_path staticmethod def verify_connection(hostname, port443, ca_fileNone): 验证 TLS 连接 context ssl.create_default_context() if ca_file: context.load_verify_locations(ca_file) try: with ssl.create_connection((hostname, port), timeout5) as sock: with context.wrap_socket(sock, server_hostnamehostname) as ssock: cert ssock.getpeercert() subject dict(x[0] for x in cert[subject]) issuer dict(x[0] for x in cert[issuer]) return { valid: True, subject: subject, issuer: issuer, notAfter: cert.get(notAfter), notBefore: cert.get(notBefore) } except ssl.SSLCertVerificationError as e: return { valid: False, error: e.verify_message, code: e.verify_code } except Exception as e: return { valid: False, error: str(e) } if __name__ __main__: import sys if len(sys.argv) 2: print(用法: python3 cert_manager.py hostname [port]) sys.exit(1) hostname sys.argv[1] port int(sys.argv[2]) if len(sys.argv) 2 else 443 print(f 证书信息: {hostname}:{port} ) print(CertificateManager.get_cert_info(hostname, port)) print(f\n 验证连接 ) result CertificateManager.verify_connection(hostname, port) if result[valid]: print(f✅ 连接安全) print(f 颁发者: {result[issuer]}) print(f 有效期: {result[notBefore]} ~ {result[notAfter]}) else: print(f❌ 验证失败: {result[error]}) print(f\n 下载证书链 ) bundle CertificateManager.download_cert_chain(hostname, port) print(f使用此证书包: export NODE_EXTRA_CA_CERTS{os.path.abspath(bundle)})4. 各方案对比总结方案适用场景推荐指数方案一系统证书企业内网⭐⭐⭐⭐⭐方案二中间证书链不完整⭐⭐⭐⭐⭐方案三禁用验证开发环境⭐⭐方案四安全网关证书企业网关⭐⭐⭐⭐方案五同步时间时间错误⭐⭐⭐⭐方案六证书工具长期运维⭐⭐⭐5. 常见问题 FAQ5.1 Windows 上证书管理方式不同Windows 使用证书管理器而非文件系统存储证书# 查看证书存储 certutil -store Root # 导出企业根证书 $cert Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object { $_.Subject -match Company } $cert | Export-Certificate -FilePath C:\company-ca.cer -Type CERT # 转换为 PEM 格式 openssl x509 -inform DER -in C:\company-ca.cer -out C:\company-ca.pem # 设置环境变量 $env:NODE_EXTRA_CA_CERTS C:\company-ca.pem openclaw 任务 # 永久设置 [System.Environment]::SetEnvironmentVariable(NODE_EXTRA_CA_CERTS, C:\company-ca.pem, User)5.2 Docker 中缺少企业根证书容器内没有企业的根证书# 将企业证书复制到容器中 FROM node:18-slim # 复制证书 COPY company-root-ca.pem /usr/local/share/ca-certificates/ RUN apt-get update apt-get install -y ca-certificates \ update-ca-certificates # 设置环境变量 ENV NODE_EXTRA_CA_CERTS/usr/local/share/ca-certificates/company-root-ca.pem # 或者在运行时挂载 # docker run -v /host/certs:/certs -e NODE_EXTRA_CA_CERTS/certs/ca.pem openclaw5.3 CI/CD 中 TLS 验证失败CI 环境可能需要不同的证书配置# GitHub Actions - 使用 secrets 传递证书 steps: - name: Configure TLS env: COMPANY_CA: ${{ secrets.COMPANY_CA_CERT }} run: | echo $COMPANY_CA /tmp/company-ca.pem export NODE_EXTRA_CA_CERTS/tmp/company-ca.pem openclaw 连接内部服务 # GitLab CI variables: NODE_EXTRA_CA_CERTS: /etc/ssl/certs/company-ca.pem before_script: - echo $COMPANY_CA_CERT /etc/ssl/certs/company-ca.pem5.4 Git 操作时 TLS 验证失败Git 使用自己的证书配置# Git 的 TLS 验证独立于 Node.js # 方法1: 配置 Git 使用企业证书 git config --global http.sslCAInfo /path/to/company-ca.pem git config --global http.sslVerify true # 方法2: 临时禁用 Git TLS 验证不推荐 git config --global http.sslVerify false # 方法3: 使用 SSH 替代 HTTPS git remote set-url origin gitgithub.com:user/repo.git # 配置 OpenClaw 的 Git 操作使用正确的证书 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[git] { sslCAInfo: /path/to/company-ca.pem, sslVerify: True } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(Git TLS 证书已配置) 5.5 企业安全网关环境下 HTTPS 请求失败企业安全网关可能重新签名所有 HTTPS 流量# 检测是否在安全网关后面 curl -v https://registry.npmjs.org/ 21 | grep -i subject\|issuer # 如果 issuer 不是真正的 CA说明安全网关在拦截 # 配置 npm 使用安全网关证书 npm config set cafile /path/to/gateway-ca.pem npm config set strict-ssl true # 配置 OpenClaw 使用安全网关证书 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[tls] { caBundlePath: /path/to/gateway-ca.pem, rejectUnauthorized: True } config[npm] { cafile: /path/to/gateway-ca.pem, strict-ssl: True } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(安全网关环境完整配置) 5.6 Lets Encrypt 证书链变更Lets Encrypt 2024 年停止使用旧根证书# 检查是否是 Lets Encrypt 旧证书问题 echo | openssl s_client -connect example.com:443 2/dev/null | \ openssl x509 -noout -issuer # 如果输出包含 DST Root CA X3说明使用的是旧根证书 # 需要 ISRG Root X1 交叉签名 # 更新 ca-certificates 包 # Ubuntu/Debian sudo apt-get update sudo apt-get install --only-upgrade ca-certificates # macOS brew install ca-certificates # 更新 Node.js 内置的 CA 列表 # 升级 Node.js 到最新版本 nvm install 20 # 或更新版本 # 或手动添加 ISRG Root X1 curl -o /tmp/isrg-root-x1.pem https://letsencrypt.org/certs/isrgrootx1.pem export NODE_EXTRA_CA_CERTS/tmp/isrg-root-x1.pem5.7 多个服务使用不同证书不同服务可能使用不同的 CA# 合并多个 CA 证书到一个文件 cat ~/.openclaw/ca-bundle.pem EOF # 企业安全网关根证书 EOF cat /etc/ssl/certs/company-internal-ca.pem ~/.openclaw/ca-bundle.pem cat ~/.openclaw/ca-bundle.pem EOF # 安全网关证书 EOF cat /etc/ssl/certs/gateway-ca.pem ~/.openclaw/ca-bundle.pem cat ~/.openclaw/ca-bundle.pem EOF # Lets Encrypt ISRG Root X1 EOF cat /tmp/isrg-root-x1.pem ~/.openclaw/ca-bundle.pem export NODE_EXTRA_CA_CERTS$HOME/.openclaw/ca-bundle.pem echo 多CA证书bundle已创建 echo 包含: 企业内网 安全网关 Lets Encrypt # 验证所有证书 for cert in company-internal-ca gateway-ca isrg-root-x1; do echo $cert openssl x509 -in ~/.openclaw/ca-bundle.pem -noout -subject 2/dev/null done5.8 OpenClaw 自身的 HTTPS 服务器证书问题如果 OpenClaw 作为服务器提供 HTTPS 服务# 生成自签名证书开发环境 openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \ -days 365 -nodes -subj /CNlocalhost # 使用 Lets Encrypt 免费证书生产环境 # certbot certonly --standalone -d openclaw.company.com # 配置 OpenClaw 的 HTTPS 服务器 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[server] { https: True, certFile: /path/to/cert.pem, keyFile: /path/to/key.pem, minTLSVersion: TLSv1.2, ciphers: TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(HTTPS 服务器证书已配置) 排查清单速查表□ 1. 设置 NODE_EXTRA_CA_CERTS 指向企业根证书 □ 2. 使用 openssl s_client 查看服务器证书链 □ 3. 下载并安装缺失的中间证书 □ 4. 检查系统时间是否正确date 命令 □ 5. 检查是否有安全网关拦截curl -v 查看证书 issuer □ 6. Docker 中挂载或复制 CA 证书 □ 7. Git 单独配置 sslCAInfo □ 8. npm 配置 cafile 和 strict-ssl □ 9. 合并多个 CA 到统一 bundle 文件 □ 10. 开发环境可用 --insecure 临时绕过6. 总结最常见原因企业内网使用自签名证书30%和证书链不完整25%首选方案设置NODE_EXTRA_CA_CERTS环境变量指向企业根证书 PEM 文件证书链修复使用openssl s_client -showcerts下载完整证书链补全缺失的中间证书安全网关环境企业安全网关拦截 HTTPS 时需要提取网关 CA 证书并添加到信任链最佳实践建议创建统一的 CA bundle 文件合并所有需要的根证书在 Docker 中通过环境变量注入避免使用NODE_TLS_REJECT_UNAUTHORIZED0仅限开发环境故障排查流程图flowchart TD A[TLS证书验证失败] -- B[检查证书链] B -- C[openssl s_client -showcerts] C -- D{证书链完整?} D --|否| E[下载缺失证书] D --|是| F[检查证书类型] E -- G[添加到NODE_EXTRA_CA_CERTS] G -- H[openclaw测试] F -- I{自签名?} I --|是| J[添加根证书到CA bundle] I --|否| K[检查过期] J -- G K -- L{已过期?} L --|是| M[更新证书] L --|否| N[检查系统时间] M -- H N -- O{时间正确?} O --|否| P[同步系统时间] O --|是| Q[检查安全网关] P -- H Q -- R{有安全网关?} R --|是| S[配置安全网关CA证书] R --|否| T[检查Node.js版本] S -- G T -- U{Node.js过旧?} U --|是| V[升级Node.js] U --|否| W[使用证书管理工具] V -- H W -- X[下载完整证书链] X -- G H -- Y{成功?} Y --|是| Z[✅ 问题解决] Y --|否| AA[开发环境用--insecure] AA -- Z