Composer的ssl.cafile必须指向含中间CA和根CA的完整PEM链文件(中间在前、根在后),需全局配置--global,确保PHP进程有读取权限,且私有仓库URL须用域名而非IP以匹配证书SAN字段。

composer config ssl.cafile 必须指向完整 PEM 链文件
很多团队把公司内网 CA 的 .crt 直接塞进 ssl.cafile,结果仍报 cURL error 60: SSL certificate problem。根本原因不是路径错,而是文件不合法:Composer 要求该路径必须指向一个「OpenSSL 可识别的完整 PEM 链」,含中间 CA 和根 CA,且顺序必须是中间在前、根在后,不能有 BOM、空行或非 PEM 内容。
实操建议:
- 用
openssl x509 -in your-ca.crt -text -noout确认文件以-----BEGIN CERTIFICATE-----开头 - 若拿到的是 Windows
.cer(DER 格式),先转 PEM:openssl x509 -inform DER -in your-ca.cer -outform PEM -out ca.pem - 合并证书链时别用记事本拼接——用
cat intermediate.pem root.pem > combined.pem更可靠 - 验证内容是否含现代根证书(如 ISRG Root X1):
grep -A1 "Subject:" combined.pem | head -20
全局配置 --global 是唯一可靠方式
在项目根目录执行 composer config ssl.cafile /path/to/ca.pem,只改当前 composer.json,但 HTTPS 仓库认证发生在网络层,所有 Composer 进程都需加载该 CA。项目级配置对 composer install 以外的命令(如 composer diagnose 或插件初始化)基本无效。
实操建议:
- 统一走全局:
composer config --global ssl.cafile /etc/ssl/certs/company-ca.pem - 确认生效:
composer config --global --list | grep ssl.cafile,输出应为实际路径 - 权限检查:PHP 进程(尤其是
www-data或nginx用户)必须有读取权限;家目录路径在 CI/CD 中常因用户隔离失效 - Composer ≥2.5 版本会静默 fallback 到系统 CA,即使配置错误也不报明确提示——务必手动验证
COMPOSER_CAFILE 环境变量优先级最高
当 ssl.cafile 配置和 php.ini 设置冲突,或需临时切换不同 CA(如多客户环境),COMPOSER_CAFILE 是最干净的解法。它绕过所有配置层,直接透传给底层 cURL 和 OpenSSL 初始化逻辑。
实操建议:
- Linux/macOS:
export COMPOSER_CAFILE="/opt/certs/internal-bundle.pem",加到~/.bashrc持久化 - Windows PowerShell:
$env:COMPOSER_CAFILE = "C:\certs\internal-bundle.pem" - CI/CD 场景中,比修改
php.ini更安全——无需重启服务,且不影响其他 PHP 应用 - 注意:该变量仅影响 CLI 模式;Web 环境需通过
putenv('COMPOSER_CAFILE=...')在入口脚本中显式注入
私有仓库 URL 必须带域名,不能写死 IP
有人为绕过 DNS 拦截,把私有仓库 URL 改成 https://192.168.10.5/packages.json,结果 PHP 报 SSL: certificate subject name '192.168.10.5' does not match target host name。这不是配置问题,是 OpenSSL 默认校验 Hostname 的行为。
实操建议:
- 私有仓库域名(如
packages.internal)必须出现在证书的Subject Alternative Name (SAN)字段中 - 若用自签名证书,生成时加参数:
openssl req -subj "/CN=packages.internal" -addext "subjectAltName = DNS:packages.internal" ... - 配合
/etc/hosts或本地 DNS 解析该域名到内网 IP,而非在 URL 中硬编码 IP - 强制出口 IP(如专线场景)要用
CURLOPT_INTERFACE+CURLOPT_RESOLVE,而不是改 URL


















