Ödeme sağlayıcısının API'sine bir istek atıyorsunuz, tarayıcıda aynı adres yeşil kilitle sorunsuz açılıyor, ama terminal size şunu döndürüyor:
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
Ya da aynı şey PHP tarafında sessizce gerçekleşiyor: curl_exec() false dönüyor, curl_error() aynı cümleyi yazıyor ve webhook'unuz iki gündür çalışmıyor. İlk refleks genelde aynıdır — arama sonuçlarının tepesindeki cevap -k eklemenizi ya da CURLOPT_SSL_VERIFYPEER değerini false yapmanızı söyler. Komut çalışır, istek geçer, konu kapanır. Kapanmaz: o satır hatayı çözmez, hatayı gösteren mekanizmayı kapatır.
Bu yazıda önce bu "çözümün" tam olarak neyi feda ettiğini göstereceğim, sonra 60 numaralı hatanın birbirinden bağımsız üç kök nedenini teşhis komutlarıyla ayıracağız: istemcideki CA deposunun eski ya da eksik olması, PHP tarafında curl.cainfo yönergesinin hiç tanımlanmamış olması ve karşı sunucunun ara sertifikayı göndermemesi.
Üçünün belirtisi tek satırlık aynı mesajdır, çözümleri ise tamamen farklı yerlerdedir. "İnternetteki komutu yapıştır" yönteminin burada özellikle işe yaramamasının sebebi budur: yanlış kök nedene uygulanan doğru komut hiçbir şey değiştirmez, siz de bir sonraki adımda doğrulamayı kapatmaya yönelirsiniz.
curl: (60) Hatası Tam Olarak Neyi Söylüyor?#
curl'ün 60 numaralı çıkış kodu CURLE_PEER_FAILED_VERIFICATION anlamına gelir: TLS el sıkışması teknik olarak kurulabildi, şifreleme çalışıyor, ama curl karşı tarafın sunduğu sertifikanın gerçekten o alan adına ait olduğunu ispatlayamadı.
İspat şöyle işler. Sunucu size kendi sertifikasını verir; o sertifika bir ara sertifika (intermediate) tarafından imzalanmıştır; ara sertifika da bir kök sertifika (root CA) tarafından imzalanmıştır. curl bu zinciri kendi makinesindeki güvenilen kök sertifika deposuna kadar takip edebilmek zorundadır. Zincirin herhangi bir halkası eksikse doğrulama başarısız olur. Konunun teorik tarafını ayrıntılı görmek isterseniz sertifika zincirinin nasıl kurulduğu iyi bir başlangıçtır.
"unable to get local issuer certificate" cümlesindeki local kelimesi kritiktir ve çoğu kişi bunu atlar: curl, sunucunun gönderdiği sertifikayı imzalayan otoriteyi kendi yerel deposunda bulamıyor. Bu iki farklı anlama gelebilir — ya sizin deponuz eksik, ya karşı taraf zincirin ortasını hiç göndermiyor. Aynı mesaj, iki ayrı arıza.
60 çıkış kodunun altında aslında birden fazla mesaj toplanır. Hangisini gördüğünüz teşhisi ciddi biçimde daraltır:
| Mesaj | Gerçek anlamı | Nerede düzeltilir |
|---|---|---|
unable to get local issuer certificate | Zincir yerel köke kadar takip edilemedi | İstemci CA deposu veya sunucu zinciri |
certificate has expired | Sertifikanın süresi dolmuş | Karşı sunucu (veya sizin saatiniz) |
certificate is not yet valid | Sertifika henüz geçerli değil | Neredeyse her zaman sistem saati |
self-signed certificate | Kendi imzalı sertifika sunuluyor | Karşı sunucu veya araya giren cihaz |
self-signed certificate in certificate chain | Zincirin içinde kendi imzalı bir halka var | Kurumsal proxy / antivirüs TLS denetimi |
subjectAltName does not match | Sertifika bu alan adı için düzenlenmemiş | Karşı sunucu yapılandırması |
Aşağıdaki bölümlerde ilk satıra, yani asıl klasik olana odaklanacağız; diğerleri için de teşhis yöntemi aynıdır.
curl -k ve CURLOPT_SSL_VERIFYPEER Neden Çözüm Değil?#
curl -k (uzun hali --insecure) veya PHP tarafında CURLOPT_SSL_VERIFYPEER => false yazdığınızda bağlantı şifreli kalmaya devam eder. Bu yüzden "zaten HTTPS, bir şey olmaz" argümanı ilk bakışta mantıklı gelir. Ama şifreleme ile kimlik doğrulama iki ayrı işlevdir ve siz ikincisini kapatmış olursunuz.
Sonuç şudur: bağlantınız artık kime kurulduğu belirsiz bir şifreli tünelden ibarettir. Araya giren biri — DNS'i zehirleyen bir saldırgan, ele geçirilmiş bir ağ cihazı, kötü yapılandırılmış bir proxy — kendi ürettiği sertifikayı sunar, curl hiç itiraz etmez, trafiğiniz saldırganın makinesinde açılır ve tekrar şifrelenip gerçek sunucuya gider. Siz hiçbir şey fark etmezsiniz; zaten hatanın kaybolmuş olması, fark etmenizi engelleyen şeyin ta kendisidir. API anahtarınız, ödeme sağlayıcısına gönderdiğiniz sipariş bilgisi ve dönen yanıt o tünelin sahibindedir.
İkinci ve daha sinsi sorun: -k hangi doğrulama hatasının olduğunu da gizler. Sertifika süresi dolduğunda, karşı taraf alan adını değiştirdiğinde ya da gerçekten araya biri girdiğinde kodunuz hepsine aynı sessizlikle karşılık verir. Doğrulamayı kapatmak yalnızca bugünkü hatayı değil, gelecekteki bütün uyarıları da kapatır.
Doğrulamayı kapatmanın savunulabilir olduğu tek yer, karşı tarafın bilerek kendi imzalı sertifika kullandığı kapalı bir ortamdır: kendi geliştirme makineniz ya da iç ağdaki bir yönetim paneli gibi. Orada bile doğru yöntem doğrulamayı kapatmak değil, o sertifikayı açıkça güvenilir kabul etmektir:
# YANLIŞ: doğrulamayı tamamen kapatır
curl -k https://ic-servis.local/health
# DOĞRU: sadece bu kökü güven, kalan her şey doğrulanmaya devam etsin
curl --cacert /etc/ssl/ic-ag-root.pem https://ic-servis.local/health
Kendi imzalı sertifika üretme ve onu güvenilir hale getirme tarafı için self-signed sertifika oluşturma yazısına bakabilirsiniz.
Hatanın Kaynağını Üç Komutla Ayırmak#
Düzeltmeye geçmeden önce hangi tarafın hatalı olduğunu bilmek gerekir. Aşağıdaki üç adım, üç kök nedeni birbirinden ayırır.
1. Adım: curl hangi CA dosyasını okuyor?#
curl -v çıktısı, doğrulamada kullanılan dosyanın yolunu açıkça yazar:
curl -v https://ornek-api.com/ 2>&1 | grep -iE "CAfile|CApath|SSL certificate"
Sağlıklı bir çıktı şuna benzer:
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs
* SSL certificate verify ok.
Buradaki dosya yolu boşsa ya da none yazıyorsa sorun kesinlikle sizin tarafınızdadır. Dosya varsa içeriğinin ne kadar eski olduğuna bakın:
# Debian / Ubuntu
ls -l /etc/ssl/certs/ca-certificates.crt
# RHEL / AlmaLinux / Rocky
ls -l /etc/pki/tls/certs/ca-bundle.crt
# OpenSSL'in derleme zamanı varsayılan dizini
openssl version -d
Dosyanın tarihi yıllar öncesine aitse birinci kök neden büyük ihtimalle sizdedir.
2. Adım: Karşı taraf zinciri eksiksiz gönderiyor mu?#
Bu, en sık atlanan adımdır ve suçun karşı tarafta olduğu durumu ortaya çıkarır:
openssl s_client -connect ornek-api.com:443 -servername ornek-api.com -showcerts </dev/null 2>/dev/null | grep -E "^(s|i):|Verify return code"
Çıktıdaki s: (subject) ve i: (issuer) satır çiftlerini sayın. Sadece tek bir sertifika dönüyorsa ve sonunda şunu görüyorsanız:
Verify return code: 21 (unable to verify the first certificate)
sunucu ara sertifikayı göndermiyor demektir. Bu durumda hata sizin makinenizde beliriyor ama düzeltme karşı sunucudadır.
Tarayıcıların aynı siteyi sorunsuz açması da tam olarak bundandır: modern tarayıcılar eksik ara sertifikayı, sertifikanın içindeki AIA alanında yazan adresten kendileri indirip zinciri tamamlar. curl bunu yapmaz, doğrulamayı olduğu gibi başarısız sayar. "Tarayıcıda çalışıyor ama curl'de çalışmıyor" tablosunun bir numaralı açıklaması budur; ayrıntı için ara sertifika hatası yazısına bakın.
3. Adım: Sistem saati doğru mu?#
Sertifikanın geçerliliği bir zaman aralığıdır; makinenin saati kayarsa geçerli bir sertifika bile reddedilir. Yeni kurulan sanal makinelerde ve uzun süre kapalı kalmış konteynerlerde klasiktir:
timedatectl status
# NTP servisi hiç yoksa en azından anlık değere bakın
date -u
System clock synchronized: no görüyorsanız önce saati düzeltin; hata büyük ihtimalle kendiliğinden kaybolur.
Kök Neden 1: Sunucudaki CA Bundle Eski veya Eksik#
En yaygın senaryo budur. Yıllardır güncellenmemiş bir sunucuda ya da minimal bir imajda kök sertifika paketi ya hiç yoktur ya da yeni kök sertifikaları içermez. Sertifika otoriteleri kök sertifikalarını yenilediğinde, eski paketler o zinciri tanıyamaz hale gelir; Let's Encrypt'in kök geçişi sırasında yüz binlerce eski sunucunun aynı anda 60 hatası vermesinin sebebi buydu.
Çözüm dağıtıma göre değişir:
# Debian / Ubuntu
sudo apt update
sudo apt install --reinstall ca-certificates
sudo update-ca-certificates
# RHEL / AlmaLinux / Rocky / CentOS Stream
sudo dnf reinstall ca-certificates
sudo update-ca-trust extract
# Alpine (Docker imajlarında en sık eksik olan paket)
apk add --no-cache ca-certificates
update-ca-certificates
Kurumsal bir kök sertifikayı kalıcı olarak güvenilir yapmak isterseniz sistem deposuna eklemek gerekir; her komuta --cacert yazmak sürdürülebilir değildir:
# Debian / Ubuntu: uzantı .crt olmak ZORUNDA, aksi halde yok sayılır
sudo cp kurumsal-root.crt /usr/local/share/ca-certificates/kurumsal-root.crt
sudo update-ca-certificates
# RHEL ailesi
sudo cp kurumsal-root.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract
Paketi güncelleyemediğiniz kısıtlı bir ortamdaysanız (paylaşımlı hosting gibi), curl'e kullanacağı dosyayı ortam değişkeniyle de gösterebilirsiniz. Kalıcı bir çözüm değildir ama -k yerine tercih edilebilecek güvenli bir ara adımdır:
export CURL_CA_BUNDLE=/home/kullanici/ssl/cacert.pem
curl https://ornek-api.com/
Kök Neden 2: PHP'de curl.cainfo Tanımsız#
Belirti çok tipiktir: terminalde curl https://... sorunsuz çalışır, ama aynı sunucudaki PHP betiği 60 hatası verir. Sebebi, komut satırı curl'ü ile PHP'nin içindeki libcurl'ün aynı ayarları paylaşmamasıdır. PHP'de curl.cainfo (ve OpenSSL akışları için openssl.cafile) yönergeleri boşsa, libcurl derleme zamanındaki varsayılan yola bakar; o yol Windows'ta hiç yoktur, bazı özel derlemelerde ise yanlıştır.
Önce mevcut durumu doğrulayın:
php -i | grep -iE "curl.cainfo|openssl.cafile|cURL support|SSL Version"
php -r "var_dump(ini_get('curl.cainfo'), ini_get('openssl.cafile'));"
İki değer de boş string dönüyorsa kök nedeni buldunuz. Düzeltme, güncel bir CA paketi indirip php.ini içinde göstermektir. Paketin resmî kaynağı curl projesinin yayınladığı cacert.pem dosyasıdır:
sudo mkdir -p /etc/ssl/php
sudo curl -o /etc/ssl/php/cacert.pem https://curl.se/ca/cacert.pem
sudo chmod 644 /etc/ssl/php/cacert.pem
Ardından php.ini dosyasına iki satır ekleyin. Mutlak yol kullanın, göreli yol çalışmaz:
; Linux
curl.cainfo = "/etc/ssl/php/cacert.pem"
openssl.cafile = "/etc/ssl/php/cacert.pem"
; Windows: ters bölü yerine düz bölü kullanın
; curl.cainfo = "C:/php/extras/ssl/cacert.pem"
; openssl.cafile = "C:/php/extras/ssl/cacert.pem"
Değişikliğin uygulanması için PHP-FPM'i ya da Apache'yi yeniden başlatın:
sudo systemctl restart php8.3-fpm
Doğru dosyayı düzenlediğinizden emin olmak için php --ini çıktısındaki "Loaded Configuration File" satırına bakın; CLI ile web isteklerini işleyen FPM havuzu çoğu kurulumda farklı php.ini okur. Bu ayrımın diğer örnekleri için php.ini ayarları yazısına göz atabilirsiniz.
Kodun içinden tek seferlik göstermek de mümkündür; bunu genel çözüm olarak değil, ortak kütüphaneye dokunamadığınız durumlar için düşünün:
$ch = curl_init('https://ornek-api.com/v1/durum');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CAINFO => '/etc/ssl/php/cacert.pem',
CURLOPT_SSL_VERIFYPEER => true, // asla false yapmayın
CURLOPT_SSL_VERIFYHOST => 2,
]);
$body = curl_exec($ch);
if ($body === false) {
error_log('curl hatasi ' . curl_errno($ch) . ': ' . curl_error($ch));
}
curl_close($ch);
cacert.pem dosyasını elle indirdiyseniz güncelliğini takip etmek de size düşer; yılda birkaç kez yenileyen küçük bir cron görevi yeterlidir.
Kök Neden 3: Karşı Sunucu Ara Sertifikayı Göndermiyor#
İkinci adımda Verify return code: 21 gördüyseniz sorun sizde değil. Sunucu yalnızca yaprak (leaf) sertifikayı gönderiyor, ara sertifikayı atlıyor. Kendi sunucunuzsa düzeltme birkaç dakikalık iştir; başkasının sunucusuysa durumu bildirmek dışında kalıcı olarak yapabileceğiniz bir şey yoktur — geçici olarak ara sertifikayı --cacert ile kendiniz sağlayabilirsiniz.
Nginx tarafında en sık yapılan hata, Let's Encrypt'in cert.pem dosyasını göstermektir. Doğrusu fullchain.pem dosyasıdır; adı zaten bunu anlatır:
server {
listen 443 ssl;
server_name ornek-api.com;
# YANLIŞ: ssl_certificate /etc/letsencrypt/live/ornek-api.com/cert.pem;
ssl_certificate /etc/letsencrypt/live/ornek-api.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ornek-api.com/privkey.pem;
}
Apache 2.4.8 ve sonrasında SSLCertificateChainFile yönergesi kullanımdan kalkmıştır; ara sertifikalar doğrudan SSLCertificateFile ile gösterilen dosyanın içinde, yaprak sertifikadan sonra yer almalıdır:
<VirtualHost *:443>
ServerName ornek-api.com
SSLEngine on
# fullchain.pem = yaprak sertifika + ara sertifikalar
SSLCertificateFile /etc/letsencrypt/live/ornek-api.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/ornek-api.com/privkey.pem
</VirtualHost>
Düzeltmeden sonra aynı openssl s_client komutunu tekrar çalıştırın; artık en az iki s:/i: çifti ve Verify return code: 0 (ok) görmelisiniz. Yapılandırmanın tamamı için nginx SSL yapılandırması ve apache SSL yapılandırması yazıları işinizi görür.
Docker ve Minimal İmajlarda Neden Sürekli Karşımıza Çıkıyor?#
scratch, alpine ve distroless gibi küçük temel imajlarda kök sertifika paketi yerden tasarruf için hiç bulunmaz. Yerelde sorunsuz çalışan bir Go, Python ya da Node uygulaması, konteyner içinde ilk HTTPS isteğinde doğrulama hatası verir. Çözüm sertifika paketini imaja eklemektir:
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
# Statik derlenmiş binary'ler için de aynı paket gerekir
COPY uygulama /usr/local/bin/uygulama
ENTRYPOINT ["/usr/local/bin/uygulama"]
Çok aşamalı (multi-stage) derlemede son aşama sıfırdan başladığı için paketi orada kurmayı unutmak çok yaygındır. docker run --rm imaj-adi ls -l /etc/ssl/certs/ca-certificates.crt komutuyla hızlıca doğrulayabilirsiniz.
Kurumsal Proxy ve Antivirüs: Zincirin İçindeki Kendi İmzalı Sertifika#
Şirket ağında çalışıyorsanız ve mesaj zincirin içinde kendi imzalı bir sertifikadan söz ediyorsa, muhtemelen bir TLS denetim cihazı veya uç nokta antivirüsü trafiği açıp yeniden şifreliyordur. Tarayıcı çalışır, çünkü kurumun kök sertifikası Windows ya da macOS sertifika deposuna dağıtılmıştır; curl, PHP, Git ve Python o depoyu kullanmadığı için hata verir.
Doğru çözüm kurumun kök sertifikasını ilgili aracın deposuna eklemektir, doğrulamayı kapatmak değil:
# Zinciri gör: en üstteki "i:" satırı kurumsal kök CA'yı gösterir
openssl s_client -connect api.ornek.com:443 -servername api.ornek.com </dev/null 2>/dev/null | grep "^i:"
# Git kendi ayarını okur, sistem deposunu otomatik kullanmayabilir
git config --global http.sslCAInfo /etc/ssl/certs/ca-certificates.crt
REQUESTS_CA_BUNDLE (Python requests), NODE_EXTRA_CA_CERTS (Node.js) ve SSL_CERT_FILE (Go ve OpenSSL tabanlı araçlar) benzer görevi görür; her ekosistem kendi değişkenine bakar, biri diğerini kapsamaz.
Belirtiden Çözüme: Hızlı Karar Tablosu#
| Belirti | Muhtemel sebep | İlk yapılacak |
|---|---|---|
| Tarayıcıda açılıyor, curl'de 60 | Sunucuda eksik ara sertifika | openssl s_client -showcerts ile zinciri say |
| CLI'da çalışıyor, PHP'de 60 | curl.cainfo tanımsız | php -i çıktısını kontrol et, php.ini düzelt |
| Yeni kurulan sunucuda her sitede 60 | Eski veya eksik CA bundle | update-ca-certificates / update-ca-trust |
| Sadece konteyner içinde 60 | ca-certificates paketi yok | İmaja paketi ekle, katmanı doğrula |
| Şirket ağında her yerde 60 | TLS denetimi yapan proxy | Kurumsal kök CA'yı ilgili depoya ekle |
| "not yet valid" mesajı | Sistem saati kaymış | timedatectl, NTP senkronunu aç |
Bu altı satır, gördüğüm 60 hatalarının neredeyse tamamını kapsar. Sıralamayı bozmadan yukarıdan aşağı ilerlemek, birbirine benzeyen belirtileri en hızlı ayıran yöntemdir. Diğer SSL hata kodlarıyla birlikte bakmak isterseniz SSL hatalarının çözümü yazısı tamamlayıcı olur.
Sıkça Sorulan Sorular#
curl -k komutu bağlantıyı şifresiz mi yapıyor?#
Hayır, bağlantı şifreli kalmaya devam eder. -k yalnızca sertifika doğrulamasını kapatır; yani veri şifrelenir ama karşı tarafın gerçekten iddia ettiği sunucu olduğu kontrol edilmez. Araya giren biri kendi sertifikasını sunarsa curl itiraz etmez ve trafiğinizi okuyabilir. Şifreleme ile kimlik doğrulama farklı işlerdir, biri diğerinin yerine geçmez.
Aynı adres tarayıcıda açılıyorken curl neden hata veriyor?#
Çoğunlukla sunucu ara sertifikayı göndermiyordur. Modern tarayıcılar eksik halkayı sertifikanın içindeki AIA adresinden kendileri indirip zinciri tamamlar; curl bu davranışı uygulamaz ve doğrulamayı olduğu gibi başarısız sayar. openssl s_client -showcerts çıktısında tek bir sertifika görüyorsanız sebep budur ve düzeltme karşı sunucudadır.
cacert.pem dosyasını nereden indirmeliyim ve ne sıklıkla güncellemeliyim?#
curl projesinin yayımladığı cacert.pem dosyası Mozilla'nın güvenilir kök listesinden üretilir ve genel amaçlı kullanım için uygundur. Linux'ta mümkünse elle indirmek yerine dağıtımın ca-certificates paketini kullanın; paket yöneticisi güncellemeyi sizin yerinize yapar. Elle indirdiyseniz yılda birkaç kez yenilemeyi bir görev olarak planlayın.
php.ini dosyasını düzenledim ama hata devam ediyor, sebebi ne olabilir?#
Büyük ihtimalle yanlış php.ini dosyasını düzenlediniz ya da PHP-FPM'i yeniden başlatmadınız. php --ini çıktısındaki "Loaded Configuration File" satırı yalnızca komut satırının kullandığı dosyayı gösterir; web isteklerini işleyen FPM havuzu çoğu kurulumda başka bir dosya okur. Doğru dosyayı düzelttikten sonra servisi yeniden başlatın ve değeri phpinfo çıktısından doğrulayın.
Let's Encrypt sertifikam var ama istemciler 60 hatası alıyor, neden?#
Web sunucusu yapılandırmasında fullchain.pem gösterilmesi gereken yerde cert.pem gösterilmesi en yaygın nedendir. cert.pem yalnızca yaprak sertifikayı içerir, ara sertifikayı içermez. Nginx'te ssl_certificate yönergesini, Apache 2.4.8 ve üzerinde ise SSLCertificateFile yönergesini fullchain.pem dosyasına yönlendirin ve servisi yeniden yükleyin.
Sertifika süresi dolmadığı halde neden "certificate is not yet valid" hatası alıyorum?#
Bu mesaj neredeyse her zaman sunucunun değil, istemcinin saatinin yanlış olduğunu gösterir. Sistem saati sertifikanın başlangıç tarihinden geriye kaymışsa, tamamen geçerli bir sertifika bile henüz geçerli değil gibi görünür. timedatectl status ile senkronizasyonu kontrol edin, NTP servisini etkinleştirin ve isteği tekrar deneyin.
Doğrulamayı kapatmadan geçici bir çözüm mümkün mü?#
Evet. Sorunun karşı sunucudaki eksik ara sertifikadan kaynaklandığını doğruladıysanız, o ara sertifikayı bir dosyaya kaydedip --cacert ya da CURLOPT_CAINFO ile açıkça verebilirsiniz. Bu yöntem yalnızca o zinciri güvenilir kılar, diğer bütün bağlantılar tam doğrulamadan geçmeye devam eder. -k ise ayrımsız her sertifikayı kabul ettiği için karşılaştırılabilir bir seçenek değildir.