DarkRiDDeR12 мин

PHP. unable to get local issuer certificate: восстановить цепочку доверия, не отключая TLS

PHPSSL

PHP-клиент не принимает сертификат, хотя сайт открывается в браузере. Цена ошибки — команда отключает проверку TLS и превращает временную проблему с CA bundle в постоянную уязвимость.

Старый рецепт часто сводится к скачиванию `cacert.pem`. В качественной версии важнее понять цепочку: какой клиент выполняет запрос, где он ищет доверенные корни, какой сертификат прислал сервер и совпадает ли окружение CLI с окружением FPM.

PHP. unable to get local issuer certificate: восстановить цепочку доверия, не отключая TLS: схема границ проверки
Иллюстрация показывает границу между симптомом, техническим механизмом и проверяемым действием.

Что сохраняем из исходной заметки

В PHP при загрузке или обмене данными с другим сервером через защищённое соединение может возникнуть ошибка:

SSL certificate error: unable to get local issuer certificate

Ошибка означает, что на сервере не установлен SSL сертификат.

Чаще всего она наблюдается, когда мы ставим локальные платформы (сервера) быстрого развёртывания для веб-разработки, таких как WAMP, XAMPP и других.

Для решения проблемы нам необходимо установить SSL сертификат.

Сертификат, например, можно взять отсюда (чтобы самим не генерировать ;):

https://curl.haxx.se/docs/caextract.html

  1. Скачиваем сертификат помещаем в папку C:\wamp\cacert.pem
    (у меня установлен WAMP, рекомендую)
  2. Включаем в  Apache mod_ssl.
  3. Добавляем расширение (ну или раскомментируем строчку) в файле php.ini:
    extension=php_openssl.dll
  4. Добавляем в 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/OpenSSLCLI и FPM могут иметь разные настройки
CA bundleПуть и дата обновленияСтарый корень даёт ложную ошибку
TLS-режимverify peer/host включеныОтключение проверки нельзя считать исправлением

Порядок действий

  1. Получить ошибку с включённым TLS verify и сохранить точный текст.
  2. Проверить цепочку сервера через `openssl s_client` или доверенный TLS checker.
  3. Узнать CA bundle для CLI и FPM, не смешивая окружения.
  4. Обновить доверенное хранилище из официального источника и задать путь через конфигурацию.
  5. Повторить запрос с `CURLOPT_SSL_VERIFYPEER=true` и `CURLOPT_SSL_VERIFYHOST=2`.
  6. Добавить проверку в deployment или health-check, чтобы bundle не устарел незаметно.

Ограничения и безопасный следующий шаг

Не скачивайте CA bundle по тому же неподтверждённому соединению, которое пытаетесь проверить.

Сертификат с самоподписанным корнем требует отдельного доверенного процесса, а не отключения проверки.

Пути `php.ini` и расширения различаются между CLI, FPM и контейнером.

После проверки должен остаться конкретный артефакт: вывод команды, тест, diff конфигурации или запись результата. Если его нет, формулировку нужно вернуть к симптому и не выдавать гипотезу за исправление.

Что записать в ревью

Короткая запись должна отвечать на четыре вопроса: какой вход использовали, какой результат увидели, какая граница была проверена и какое действие разрешено дальше. Такая форма полезнее длинного вывода «всё работает»: другой инженер сможет повторить проверку и понять, где заканчивается пример.

Если результат зависит от версии Windows, PHP, Bitrix, D или браузера, версию фиксируем рядом с командой. Если проверка не охватывает сеть, production или реальные пользовательские данные, это ограничение пишем прямо. Тогда следующий шаг расширяет evidence, а не расширяет обещание.

Проверяемые источники