状況の説明
外部API(Stripe、PayPal、Webhookエンドポイントなど)にHTTPSリクエストを送ると、PHPが以下のエラーをスローします:
cURL error 60: SSL certificate problem: unable to get local issuer certificate
PostmanやブラウザではOKなのに、PHPだけが毎回拒否します。このエラーは、CAバンドルが存在しないか無効なパスを指している、Windows開発環境(XAMPP、Laragon、WAMP)や新規プロビジョニングされたLinuxサーバーでほぼ必ず発生します。
原因
cURLはサーバーのSSL証明書を、信頼された認証局(CA)のリストと照合して検証します。Linuxでは、そのバンドルは/etc/ssl/certs/ca-certificates.crt(Debian/Ubuntu)または/etc/pki/tls/certs/ca-bundle.crt(CentOS/RHEL)にあります。問題が起きる原因は2つです:PHPのcURLがファイルを見つけられないか、システムのCAバンドルが古すぎて接続先の証明書の発行者が含まれていないかのどちらかです。
Windowsは事情が異なります。cURLはデフォルトでWindowsの証明書ストアを読み込まず、php.iniで明示的に指定したcacert.pemが必要です。XAMPPはデフォルトでcainfoの設定がされていないため、外部APIへのすべてのHTTPS呼び出しがエラー60で失敗します。
応急処置(開発環境のみ — 本番環境では使用禁止)
今すぐ動作確認したい場合は、SSL検証を無効にしてください。本番環境では絶対に行わないでください。
$ch = curl_init('https://api.example.com/endpoint');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 開発環境のみ
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 開発環境のみ
$response = curl_exec($ch);
curl_close($ch);
Guzzleの場合:
$client = new \GuzzleHttp\Client(['verify' => false]); // 開発環境のみ
$response = $client->get('https://api.example.com/endpoint');
レスポンスが返ってきましたか?それならSSL証明書の検証が原因であることが確認できました。次は正しく修正しましょう。
恒久的な解決策 — cURLに有効なCAバンドルを指定する
ステップ1:Mozilla CAバンドルをダウンロードする
curlプロジェクトはMozillaから直接取得した公式のcacert.pemを管理しています。年に数回更新され、主要なCAをすべてカバーしています:
# Linux
wget -O /etc/ssl/certs/cacert.pem https://curl.se/ca/cacert.pem
# curlを使う場合
curl -o /etc/ssl/certs/cacert.pem https://curl.se/ca/cacert.pem
Windowsの場合は手動でダウンロードし、安定した場所に配置してください。C:\php\extras\cacert.pemが適切です。スペースを含むパスは避けてください。
ステップ2:PHPに使用するCAバンドルを指定する
php.iniを編集します(正しいファイルはphp --iniで確認するか、ブラウザでphpinfo()を確認してください):
[curl]
curl.cainfo = /etc/ssl/certs/cacert.pem
[openssl]
openssl.cafile = /etc/ssl/certs/cacert.pem
XAMPPを使用しているWindowsでは、引用符付きのWindowsスタイルのパスを使用してください:
[curl]
curl.cainfo = "C:\php\extras\cacert.pem"
[openssl]
openssl.cafile = "C:\php\extras\cacert.pem"
保存後、サーバーを再起動してください:
# Linux systemd
sudo systemctl restart php8.2-fpm
sudo systemctl restart apache2
# Windows上のXAMPP — コントロールパネルから再起動
ステップ3:リクエストごとに設定する(共有ホスティングのフォールバック)
php.iniにアクセスできない場合は、cURL呼び出しで直接CAバンドルのパスを渡してください:
$ch = curl_init('https://api.example.com/endpoint');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/etc/ssl/certs/cacert.pem');
$response = curl_exec($ch);
if ($response === false) {
echo curl_error($ch);
}
curl_close($ch);
Guzzleでも同様です:
$client = new \GuzzleHttp\Client([
'verify' => '/etc/ssl/certs/cacert.pem'
]);
$response = $client->get('https://api.example.com/endpoint');
Linux:システムCAストアを更新する方法
DebianとUbuntuでは、CA証明書パッケージを再インストールしてバンドルを再生成するだけで解決できます:
sudo apt-get update
sudo apt-get install --reinstall ca-certificates
sudo update-ca-certificates
CentOS/RHELの場合:
sudo yum reinstall ca-certificates
sudo update-ca-trust
これにより、システムバンドルを参照するようにコンパイルされたPHPのcURLは変更を自動的に反映します。php.iniの編集は不要です。
修正が成功したか確認する
アプリのコードを触る前に、CLIからテストしてください:
# curlで直接テスト
curl -v https://api.example.com/endpoint
# PHPのcURLで確認
php -r "
\$ch = curl_init('https://api.example.com/endpoint');
curl_setopt(\$ch, CURLOPT_RETURNTRANSFER, true);
\$out = curl_exec(\$ch);
\$err = curl_error(\$ch);
curl_close(\$ch);
echo \$err ? 'ERROR: ' . \$err : 'OK — got ' . strlen(\$out) . ' bytes';
"
OK — got N bytesと表示されれば成功です。まだ失敗する場合は、php --iniを実行して正しいphp.iniが読み込まれているか確認してください。次にブラウザでphpinfo()を開き、curl.cainfoを検索してください。そこに表示されているパスが、PHPが実際に使用しているパスです(編集したファイルと異なる場合があります)。
自己署名証明書または内部CA証明書
自己署名証明書を使用した内部APIを呼び出す場合、検証を無効にしてはいけません。それは一つの問題を、より深刻な問題と交換するだけです。代わりに、内部CA証明書をバンドルに追記してください:
# 内部CA証明書をcacert.pemバンドルに追記する
cat /path/to/your-internal-ca.crt >> /etc/ssl/certs/cacert.pem
またはリクエストごとにcURLへ直接指定する方法もあります:
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/your-internal-ca.crt');
クイックリファレンス
- Windows開発環境(XAMPP/Laragon):
cacert.pemをダウンロードし、php.iniでcurl.cainfoを設定する - Linux本番サーバー:
update-ca-certificatesを実行するか、php.iniでcurl.cainfoを設定する - **自己署名証明書:**CAをバンドルに追記する — 検証を無効化しない
- **
php.iniにアクセスできない場合:**リクエストごとにCURLOPT_CAINFOを使用するか、Guzzleで'verify' => '/path/to/cacert.pem'を指定する - 本番環境では絶対に使用しない:
CURLOPT_SSL_VERIFYPEER = false

