Sửa lỗi PHP cURL Error 60: SSL Certificate Problem Unable to Get Local Issuer Certificate

intermediate🐘 PHP2026-05-10| PHP 7.x/8.x trên Windows (XAMPP/Laragon/WAMP) và Linux (Ubuntu/CentOS/Debian), mọi framework dùng Guzzle hoặc curl_exec() trực tiếp

Error Message

cURL error 60: SSL certificate problem: unable to get local issuer certificate
#php#curl#ssl#https#certificate#api

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, set curl.cainfo trong php.ini
  • Server Linux production: Chạy update-ca-certificates hoặc set curl.cainfo trong php.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ùng CURLOPT_CAINFO theo 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

Related Error Notes