composer 的 ssl.cafile 必须指向完整 pem 格式证书链文件,含根 ca 与中间 ca,顺序为中间 ca 在前、根 ca 在后,且需全局配置(–global)并确保 php 进程有读取权限。

composer config ssl.cafile 指向的证书必须是 PEM 格式合并文件
很多团队把公司内网 CA 的 .crt 或 .pem 直接丢进 ssl.cafile,结果 composer 仍报 curl Error 60: SSL certificate problem。根本原因是:Composer 要求该路径指向一个「完整、可被 OpenSSL 识别的 PEM 文件」,它得包含完整的证书链(根 CA + 中间 CA),且不能混入非 PEM 内容(比如注释、空行、Base64 外的文本)。
实操建议:
- 用
openssl x509 -in your-ca.crt -text -noout确认文件确实是 PEM 编码(以-----BEGIN CERTIFICATE-----开头) - 如果拿到的是 windows 的
.cer或二进制 DER,先转成 PEM:openssl x509 -inform DER -in your-ca.cer -outform PEM -out ca.pem - 若私有仓库用了中间 CA,需把根 CA 和中间 CA 合并到一个文件(顺序:站点证书 → 中间 CA → 根 CA),但 composer 只认 CA 部分,所以只放中间 + 根即可
- 别用编辑器手动拼接——换行符或 bom 会导致解析失败;用
cat intermediate.pem root.pem > combined.pem更可靠
全局设置 vs 项目级设置:优先用 –global 配置
很多人在项目根目录跑 composer config ssl.cafile /path/to/ca.pem,结果其他项目或 CLI 下的 composer 命令仍不生效。这是因为该命令默认只改当前项目的 composer.json,而 https 仓库认证发生在全局网络层,必须让所有 composer 进程都加载这个 CA。
实操建议:
- 统一走全局配置:
composer config --global ssl.cafile /etc/ssl/certs/company-ca.pem - 确认生效:
composer config --global --list | grep ssl.cafile,输出应为实际路径 - 如果公司强制要求项目隔离(比如多客户环境),才考虑项目级配置,但必须确保
composer install时未启用--no-plugins(某些自定义插件会干扰 CA 加载) - 注意权限:PHP 进程需有读取该文件的权限,尤其是用 nginx/php-fpm 时,
www-data用户可能无权读取家目录下的文件
composer 2.5+ 对 ssl.cafile 的加载逻辑变了
旧版(ssl.cafile 直接透传给 cURL;新版(≥2.5)会先尝试用它初始化 OpenSSL 上下文,再 fallback 到系统 CA 路径。这意味着:即使你配了 ssl.cafile,如果文件路径错、格式错、或权限不足,composer 不报明确错误,而是静默忽略,继续用系统默认 CA——然后继续报 60 错误,让人误以为“配置没生效”。
实操建议:
- 加个快速验证步骤:
php -r "var_dump(openssl_get_cert_locations());",看default_cert_file是否包含你设的路径(新版不会显示,但可确认 OpenSSL 本身能否读取) - 临时加日志:运行
composer -v install,观察是否有Reading CA bundle from ...类似提示(有则说明路径被识别,无则大概率路径无效) - 绕过验证仅用于排查:
composer config --global secure-http false(⚠️仅调试用,切勿提交或长期开启)
私有 Packagist 服务(如 Satis、Private Packagist)还需额外处理
单纯配 ssl.cafile 只解决「下载包时的 HTTPS 连接」,但如果你还用了自建的 Packagist 镜像(比如 Satis),它的 repositories 元数据本身也是通过 HTTPS 拉取的——这部分请求由 Composer 自己发起,也走同一套 CA 链,但有个隐藏坑:Satis 生成的 packages.json 若含 dist.url 字段,且该 URL 是 HTTP(而非 HTTPS),composer 会跳过 CA 校验,导致你以为“CA 配好了”,其实根本没走 HTTPS 流程。
实操建议:
- 检查
packages.json里所有dist.url是否真为https://开头,不是http://或相对路径 - Satis 配置中显式指定
"secure-http": true,并在output-dir部署后,用curl -I https://your-satis/repo/packages.json确认能通且返回 200 - 若用 Nginx 反代 Satis,确保
proxy_ssl_trusted_certificate指向同一份combined.pem,否则反代层就校验失败了
最常被忽略的一点:证书文件路径在 Docker 容器里要映射进去,宿主机配了 --global ssl.cafile,但容器里 PHP 根本看不到那个路径——这时候光配 config 没用,得挂载 + 重新 config。