Tình Huống Xảy Ra
Bạn thực hiện request HTTPS đến một API bên ngoài — Stripe, PayPal, một webhook endpoint nào đó — và PHP ném ra lỗi này:
cURL error 60: SSL certificate problem: unable to get local issuer certificate
Postman chạy ngon. Trình duyệt cũng chạy được. Riêng PHP từ chối. Mọi lần đều vậy. Lỗi này hầu như luôn xảy ra trên máy dev Windows (XAMPP, Laragon, WAMP) hoặc các server Linux mới được cấu hình mà CA bundle bị thiếu hoặc trỏ đến chỗ không có gì.
Nguyên Nhân
cURL xác minh chứng chỉ SSL của server dựa vào danh sách các Certificate Authority (CA) đáng tin cậy. Trên Linux, bundle đó nằm tại /etc/ssl/certs/ca-certificates.crt (Debian/Ubuntu) hoặc /etc/pki/tls/certs/ca-bundle.crt (CentOS/RHEL). Có hai vấn đề thường gặp: hoặc là cURL đi kèm PHP không tìm thấy file này, hoặc CA bundle của hệ thống đã quá lỗi thời và không bao gồm issuer cho chứng chỉ bạn đang kết nối tới.
Windows lại là câu chuyện khác. cURL mặc định không đọc Windows certificate store — nó cần một file cacert.pem riêng được trỏ đường dẫn tường minh trong php.ini. Theo mặc định, XAMPP không có cấu hình cainfo, vì vậy mọi request HTTPS đến API bên ngoài đều thất bại với error 60.
Cách Sửa Nhanh (Chỉ Dùng Khi Dev — Đừng Đưa Lên Production)
Cần gỡ chặn ngay lập tức? Tắt xác minh SSL đi. Tuyệt đối không làm vậy trên production.
$ch = curl_init('https://api.example.com/endpoint');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // chỉ dùng khi dev
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // chỉ dùng khi dev
$response = curl_exec($ch);
curl_close($ch);
Với Guzzle:
$client = new \GuzzleHttp\Client(['verify' => false]); // chỉ dùng khi dev
$response = $client->get('https://api.example.com/endpoint');
Có response rồi? Tốt. Điều đó xác nhận lỗi SSL cert check là thủ phạm. Giờ hãy sửa đúng cách.
Sửa Triệt Để — Trỏ cURL đến CA Bundle Hợp Lệ
Bước 1: Tải Mozilla CA Bundle
Dự án curl duy trì một file cacert.pem chính thức lấy trực tiếp từ Mozilla. File này được cập nhật vài lần mỗi năm và bao gồm tất cả các CA lớn:
# Linux
wget -O /etc/ssl/certs/cacert.pem https://curl.se/ca/cacert.pem
# Hoặc dùng chính curl
curl -o /etc/ssl/certs/cacert.pem https://curl.se/ca/cacert.pem
Trên Windows, tải thủ công và đặt vào thư mục ổn định — C:\php\extras\cacert.pem là lựa chọn tốt. Tránh đường dẫn có khoảng trắng.
Bước 2: Cấu Hình PHP Sử Dụng File Đó
Chỉnh sửa php.ini (tìm file đúng bằng php --ini hoặc kiểm tra phpinfo() trên trình duyệt):
[curl]
curl.cainfo = /etc/ssl/certs/cacert.pem
[openssl]
openssl.cafile = /etc/ssl/certs/cacert.pem
Trên Windows với XAMPP, dùng đường dẫn kiểu Windows có dấu ngoặc kép:
[curl]
curl.cainfo = "C:\php\extras\cacert.pem"
[openssl]
openssl.cafile = "C:\php\extras\cacert.pem"
Khởi động lại server sau khi lưu:
# Linux systemd
sudo systemctl restart php8.2-fpm
sudo systemctl restart apache2
# XAMPP trên Windows — khởi động lại qua control panel
Bước 3: Cấu Hình Từng Request (Dành Cho Shared Hosting)
Không có quyền truy cập php.ini? Truyền đường dẫn CA bundle trực tiếp vào từng cURL call:
$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);
Tương tự với Guzzle:
$client = new \GuzzleHttp\Client([
'verify' => '/etc/ssl/certs/cacert.pem'
]);
$response = $client->get('https://api.example.com/endpoint');
Linux: Cập Nhật System CA Store Thay Thế
Debian và Ubuntu có cách đơn giản hơn — chỉ cần cài lại gói CA certificates và tạo lại bundle:
sudo apt-get update
sudo apt-get install --reinstall ca-certificates
sudo update-ca-certificates
Tương đương trên CentOS/RHEL:
sudo yum reinstall ca-certificates
sudo update-ca-trust
Sau đó, cURL của PHP (nếu được biên dịch để đọc system bundle) tự động nhận các thay đổi. Không cần chỉnh sửa php.ini.
Kiểm Tra Xem Đã Sửa Được Chưa
Kiểm tra từ CLI trước khi đụng vào code ứng dụng:
# Kiểm tra trực tiếp với curl
curl -v https://api.example.com/endpoint
# Kiểm tra cURL của PHP
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';
"
Bạn muốn thấy OK — got N bytes. Vẫn lỗi? Chạy php --ini và xác nhận đang load đúng file php.ini. Sau đó mở phpinfo() trên trình duyệt và tìm curl.cainfo — đường dẫn hiển thị ở đó mới là thứ PHP thực sự đang dùng, không nhất thiết là file bạn vừa chỉnh.
Chứng Chỉ Self-Signed hoặc Internal CA
Đang gọi đến API nội bộ đứng sau chứng chỉ self-signed? Đừng tắt xác minh — làm vậy là đổi một vấn đề lấy một vấn đề tệ hơn. Thay vào đó, hãy thêm CA cert nội bộ của bạn vào bundle:
# Thêm CA cert nội bộ vào cacert.pem bundle
cat /path/to/your-internal-ca.crt >> /etc/ssl/certs/cacert.pem
Hoặc trỏ cURL trực tiếp vào file đó theo từng request:
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/your-internal-ca.crt');
Tóm Tắt Nhanh
- Máy dev Windows (XAMPP/Laragon): Tải
cacert.pem, setcurl.cainfotrongphp.ini - Server Linux production: Chạy
update-ca-certificateshoặc setcurl.cainfotrongphp.ini - Chứng chỉ self-signed: Thêm CA của bạn vào bundle — không bao giờ tắt xác minh
- Không có quyền truy cập
php.ini: DùngCURLOPT_CAINFOtheo từng request, hoặc'verify' => '/path/to/cacert.pem'trong Guzzle - Tuyệt đối không dùng trên production:
CURLOPT_SSL_VERIFYPEER = false

