PHP-клиент не принимает сертификат, хотя сайт открывается в браузере. Цена ошибки — команда отключает проверку TLS и превращает временную проблему с CA bundle в постоянную уязвимость.
Старый рецепт часто сводится к скачиванию `cacert.pem`. В качественной версии важнее понять цепочку: какой клиент выполняет запрос, где он ищет доверенные корни, какой сертификат прислал сервер и совпадает ли окружение CLI с окружением FPM.
Что сохраняем из исходной заметки
В PHP при загрузке или обмене данными с другим сервером через защищённое соединение может возникнуть ошибка:
SSL certificate error: unable to get local issuer certificate
Ошибка означает, что на сервере не установлен SSL сертификат.
Чаще всего она наблюдается, когда мы ставим локальные платформы (сервера) быстрого развёртывания для веб-разработки, таких как WAMP, XAMPP и других.
Для решения проблемы нам необходимо установить SSL сертификат.
Сертификат, например, можно взять отсюда (чтобы самим не генерировать ;):
https://curl.haxx.se/docs/caextract.html
- Скачиваем сертификат помещаем в папку C:\wamp\cacert.pem
(у меня установлен WAMP, рекомендую) - Включаем в Apache mod_ssl.
- Добавляем расширение (ну или раскомментируем строчку) в файле php.ini:
extension=php_openssl.dll - Добавляем в php.ini также параметры:
curl.cainfo=»C:/wamp/cacert.pem»
openssl.cafile=»C:/wamp/cacert.pem»
Перезагружаем сервер. После чего ошибка должна быть решена.
Финальная проверка настройки
После подключения файла сертификатов перезапустите веб-сервер или PHP-FPM и выполните тестовый HTTPS-запрос из того же окружения, где возникала ошибка. Важно проверять не браузер, а именно PHP: CLI и веб-сервер могут использовать разные php.ini.
php --ini
php -i | grep -E "curl.cainfo|openssl.cafile"
php -r "var_dump(file_get_contents('https://example.com') !== false);"
Для cURL указывается curl.cainfo, для потоков OpenSSL — openssl.cafile. На Windows путь лучше писать полностью и без относительных директорий. Если используется не глобальный php.ini, а отдельная настройка клиента, можно передать CA bundle прямо в cURL:
curl_setopt($ch, CURLOPT_CAINFO, 'C:\php\extras\ssl\cacert.pem');
Не отключайте проверку SSL через CURLOPT_SSL_VERIFYPEER = false как постоянное решение. Это скрывает проблему и делает соединение уязвимым. Такая настройка допустима только для короткой диагностики в локальной среде, и после проверки её нужно вернуть обратно.
Если CA bundle указан верно, но ошибка осталась, проверьте цепочку сертификатов на стороне удалённого сервера. Иногда браузер открывает сайт, потому что умеет достраивать цепочку, а PHP/cURL получает неполную цепочку и честно падает. В таком случае исправлять нужно сертификаты сервера, а не PHP-код.
Правильный итог: актуальный cacert.pem, явно указанные curl.cainfo и openssl.cafile, перезапуск PHP и тест именно из того окружения, где работает приложение.
Механизм без лишних обещаний
Клиент проверяет не только имя хоста. Он строит цепочку от сертификата сервера до доверенного корня и сверяет срок, имя и подпись. Если промежуточный сертификат не пришёл или локальное хранилище устарело, появляется `unable to get local issuer certificate`.
Браузер и PHP могут использовать разные хранилища. Поэтому «в браузере зелёный замок» не доказывает, что CLI-cURL видит тот же CA bundle.
Исправление должно менять путь к актуальному bundle или хранилище ОС. Отключение `CURLOPT_SSL_VERIFYPEER` и `CURLOPT_SSL_VERIFYHOST` убирает доказательство подлинности и не является диагностикой.
Минимальный воспроизводимый пример
Ниже — маленькая проверка, которую можно запустить или адаптировать в отдельном тестовом окружении. Значения демонстрационные; проектные идентификаторы, пути и версии нужно заменить своими и сохранить рядом с результатом.
<?php
$ch = curl_init("https://example.test/health");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CAINFO => __DIR__ . "/certs/cacert.pem",
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException(curl_error($ch));
}
curl_close($ch);
Матрица диагностики
| Проверка | Что смотрим | Почему это важно |
|---|---|---|
| Сервер | Цепочка сертификатов и имя | Клиент должен получить промежуточный сертификат |
| PHP | Версия и cURL/OpenSSL | CLI и FPM могут иметь разные настройки |
| CA bundle | Путь и дата обновления | Старый корень даёт ложную ошибку |
| TLS-режим | verify peer/host включены | Отключение проверки нельзя считать исправлением |
Порядок действий
- Получить ошибку с включённым TLS verify и сохранить точный текст.
- Проверить цепочку сервера через `openssl s_client` или доверенный TLS checker.
- Узнать CA bundle для CLI и FPM, не смешивая окружения.
- Обновить доверенное хранилище из официального источника и задать путь через конфигурацию.
- Повторить запрос с `CURLOPT_SSL_VERIFYPEER=true` и `CURLOPT_SSL_VERIFYHOST=2`.
- Добавить проверку в deployment или health-check, чтобы bundle не устарел незаметно.
Ограничения и безопасный следующий шаг
Не скачивайте CA bundle по тому же неподтверждённому соединению, которое пытаетесь проверить.
Сертификат с самоподписанным корнем требует отдельного доверенного процесса, а не отключения проверки.
Пути `php.ini` и расширения различаются между CLI, FPM и контейнером.
После проверки должен остаться конкретный артефакт: вывод команды, тест, diff конфигурации или запись результата. Если его нет, формулировку нужно вернуть к симптому и не выдавать гипотезу за исправление.
Что записать в ревью
Короткая запись должна отвечать на четыре вопроса: какой вход использовали, какой результат увидели, какая граница была проверена и какое действие разрешено дальше. Такая форма полезнее длинного вывода «всё работает»: другой инженер сможет повторить проверку и понять, где заканчивается пример.
Если результат зависит от версии Windows, PHP, Bitrix, D или браузера, версию фиксируем рядом с командой. Если проверка не охватывает сеть, production или реальные пользовательские данные, это ограничение пишем прямо. Тогда следующий шаг расширяет evidence, а не расширяет обещание.
Проверяемые источники
- PHP: cURL configuration — описывает `curl.cainfo` и настройки расширения
- curl: SSL certificate verification — объясняет CA store и цепочку доверия
- OpenSSL: verify — помогает проверить сертификат вне PHP