Mert Özen Her Satırda Daha İleri
DevOps SSL/TLS Certificate'ları ve HTTPS — #08

Certificate'ları İzlemek ve Kesintileri Önlemek: Envanter, Expiry Alert'leri, TLS Testleri ve Gerçek Olay Örnekleri

Mert Özen 14 Kas 2026 15 dk 3 görüntülenme
Certificate'ları İzlemek ve Kesintileri Önlemek: Envanter, Expiry Alert'leri, TLS Testleri ve Gerçek Olay Örnekleri

Certificate kesintilerini önlemek için diskteki dosyayı değil, istemcinin gördüğü certificate'ı dışarıdan ölçersin: blackbox exporter'ın last_chain metric'ine ömre göre eşikli bir alarm, probe_success alarmı, CT log ve CAA ile envanter. Reload edilmemiş yenilemeyi, eksik intermediate'ı, yanlış SAN'ı, earliest_cert_expiry'yi yanıltan cross certificate'ı ve saat kaymasını laboratuvarda üretiyor, alarmları promtool ile, TLS config'ini testssl.sh ile test ediyoruz.

Sürüm notu: Bu yazıdaki örnekler Prometheus blackbox_exporter 0.29.1, Prometheus 3.16 (promtool), testssl.sh 3.2.5, Nginx 1.30.6 ve OpenSSL 3.5 ile Docker 29 üzerinde denenmiştir; crt.sh ve DNS verileri 14 Kasım 2026'da alınmıştır.

Önce Tahmin Et

Pazartesi sabahı iki mesaj geliyor. Birincisi müşteri destekten: "Site certificate hatası veriyor." Oysa Certbot'un gece log'unda yenilemenin başarılı olduğu yazıyor ve diskteki fullchain.pem'in bitişi 47 gün sonrası. İkincisi monitoring'den: api.bookstore.example için "earliest certificate expiry: 2 gün" uyarısı. Ama tarayıcıda certificate'a bakan herkes onun 46 gün daha geçerli olduğunu görüyor.

Tahmin et: birinci olayda certificate gerçekten yenilenmiş mi, sorun nerede? İkinci uyarı yanlış alarm mı, yoksa birilerinin canını yakacak gerçek bir sorun mu? İkisini de laboratuvarda yeniden üreteceğiz.

Envanter: Hangi Certificate Nerede, Kimin Elinde?

İzleyemediğin certificate'ı yenileyemezsin, varlığından haberin olmayanı da izleyemezsin. Serinin önceki yazılarında Bookstore'un certificate'ları yedi farklı yerde yaşadı: Nginx + Certbot, Kestrel, Caddy, App Service, Front Door, cert-manager ve internal CA'nın (sertifika veren güvenilir kuruluş) partner certificate'ları. Envanterin her satırı şu soruları cevaplamalı: hangi isim (SAN, Subject Alternative Name), hangi CA ve profil, kim yeniliyor (araç ve sahibi olan ekip), nerede kuruluyor (hangi sunucu, Secret, binding), yenilendiğini kim fark eder ve bitiş tarihi ne? Son iki soru en sık boş kalanlar.

Envanteri elle tutmak yerine kaynaklardan topla. cert-manager'da kubectl get certificate -A, Azure'da Resource Graph sorgusu, Certbot'ta certbot certificates. Bir de dışarıdan bakan bir kaynak gerekiyor: Certificate Transparency (CT) log'ları. İkinci yazıda crt.sh ile bir domain'in certificate'larını listelemiştik. Bunu bir kontrole çevirmek için, CT'deki issuer'ları bir izin listesiyle karşılaştıran küçük bir script yazdım. Örnek olarak IANA'nın (internet adres ve numara kayıtlarını yöneten kuruluş) test domain'i example.com'u, yalnızca Sectigo'ya izin veriyormuşuz gibi kontrol ettim:

#!/bin/sh
# CT log'larındaki süresi dolmamış certificate'ların issuer'larını bir izin listesiyle karşılaştırır
domain=$1; allowed=$2
curl -sf --retry 3 "https://crt.sh/?q=${domain}&output=json&exclude=expired&deduplicate=Y" \
  | jq -r --arg allowed "$allowed" '
      group_by(.issuer_name)[]
      | {issuer: .[0].issuer_name, count: length, newest: (map(.not_before) | max)}
      | (if (.issuer | test($allowed)) then "ok        " else "UNEXPECTED" end)
        + "  \(.count)  newest=\(.newest)  \(.issuer)"'
$ ./ct-check.sh example.com "Sectigo"
ok          1  newest=2026-09-24T00:00:00  C=GB, O=Sectigo Limited, CN=Sectigo Public Server Authentication CA DV E36
ok          3  newest=2025-12-02T00:00:00  C=GB, O=Sectigo Limited, CN=Sectigo Public Server Authentication CA OV R36
UNEXPECTED  1  newest=2021-08-31T15:55:23  C=GB, ST=Cymru, O=AS207960 Cyfyngedig, OU=AS207960 Department of Random Numbers, CN=AS207960 Test CA
UNEXPECTED  2  newest=2026-09-26T22:49:11  C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
UNEXPECTED  1  newest=2023-01-27T01:21:18  Issuer Not Found

Kendi domain'inde böyle bir satır, ya envanterde olmayan bir ekibi (bir CDN'e (statik dosyaları kullanıcıya yakın sunuculardan dağıtan ağ) geçmiş bir pazarlama sitesi gibi) ya da senin adına certificate alan birini gösterir. İkisi de bilmen gereken şeyler. Bu script'i günlük bir job olarak çalıştırıp UNEXPECTED satırlarını alarma bağlayabilir ya da aynı işi yapan bir CT monitoring servisi kullanabilirsin. CT ise yalnızca olanı bildirir; önlemek için CAA (hangi CA'ların sertifika verebileceğini belirten DNS kaydı) kaydı var. DNS'te hangi CA'ların senin domain'in için certificate verebileceğini yazarsın:

$ dig +short CAA google.com
0 issue "pki.goog"

Beşinci yazıdaki App Service ve Front Door managed certificate'ları DigiCert'ten geliyordu. CAA kaydına onu eklemeyi unutursan yenileme kırılır. CAA kayıtlarının kurulumu Domain, DNS ve CDN serisinin konusu.

Sunulan Certificate'ı Dışarıdan Ölçmek: Blackbox Exporter

Envanter "ne olmalı"yı söyler. İzleme ise "ne oluyor"u ölçmeli, yani istemcinin gerçekten gördüğü certificate'ı. Diskteki dosyaya, cert-manager'ın Secret'ına ya da ACME (sertifika alma ve yenilemeyi otomatikleştiren protokol) client'ının log'una bakmak yetmez; bunların hepsi doğru olup sunucu yine de eski certificate'ı sunabilir. Prometheus'un blackbox exporter'ı hedefe gerçek bir HTTPS (HTTP Secure) isteği atıp TLS bilgisini metric olarak veriyor:

modules:
  https_2xx:
    prober: http
    timeout: 5s
    http:
      fail_if_not_ssl: true
      tls_config:
        ca_file: /certs/root.crt

ca_file yalnızca labın test CA'sı için var. Public certificate'larda bu satırı yazmazsın ve exporter sistem trust store'unu kullanır. Prometheus normalde /probe endpoint'ini scrape eder; ben aynı endpoint'i elle çağırdım:

#!/bin/sh
# blackbox exporter'a hedefi yoklatır ve TLS metric'lerini yazar
docker exec tlsseries-blackbox wget -qO- "http://localhost:9115/probe?module=https_2xx&target=$1" \
  | grep -E '^probe_(success|ssl_earliest_cert_expiry|ssl_last_chain_expiry_timestamp_seconds|ssl_last_chain_info|tls_version_info)'
$ ./probe.sh https://www.bookstore.example
probe_ssl_earliest_cert_expiry 1.795474915e+09
probe_ssl_last_chain_expiry_timestamp_seconds 1.795474915e+09
probe_ssl_last_chain_info{fingerprint_sha256="f572df4f37dffeeec1b92c908a8c6e06df6e096225c43d2ab780be4c7a0cde1c",issuer="CN=Bookstore Lab Issuing CA",serialnumber="073426772d270b25ba70e17801befddb98387be9",subject="CN=www.bookstore.example",subjectalternative="www.bookstore.example,bookstore.example"} 1
probe_success 1
probe_tls_version_info{version="TLS 1.3"} 1

İki zaman damgası da Unix epoch: 1795474915, 23 Kasım 2026 23:01:55 UTC (Coordinated Universal Time). probe_ssl_last_chain_info sunulan leaf'in seri numarasını ve SAN listesini label olarak taşıyor; "yenilendi ama sunulmuyor" sorununu bununla yakalayacağız. probe_success ise en az expiry kadar önemli: zincir doğrulanamazsa ya da isim tutmazsa probe başarısız olur ve expiry metric'leri hiç üretilmez.

earliest_cert_expiry mi, last_chain_expiry mi?

Önce Tahmin Et'in ikinci sorusu. İki metric'in farkı, exporter'ın kaynak kodunda tek satır. probe_ssl_earliest_cert_expiry, sunucunun gönderdiği bütün certificate'ların (PeerCertificates) en erken bitiş tarihi. probe_ssl_last_chain_expiry_timestamp_seconds ise istemcinin doğrulayabildiği zincirlerin (VerifiedChains) her biri için en erken bitişi bulup en iyisini alıyor. Farkı görmek için labda eski bir root'la, Bookstore'un root'unu çapraz imzalattım (cross-sign) ve bu cross certificate'a iki günlük bir ömür verdim. Sunucu leaf, intermediate ve cross certificate'ı birlikte gönderiyor:

$ cat new.crt int.crt root-cross.crt > live/fullchain.pem && nginx -s reload
$ ./probe.sh https://www.bookstore.example | grep -E "success|expiry"
probe_ssl_earliest_cert_expiry 1.794783715e+09
probe_ssl_last_chain_expiry_timestamp_seconds 1.798671715e+09
probe_success 1

İlki 15 Kasım 2026, ikincisi 30 Aralık 2026. Root'a doğrudan güvenen istemci, zinciri cross certificate'ı hiç kullanmadan kuruyor ve sorun yaşamıyor. Bu, Önce Tahmin Et'teki uyarının aynısı. Ama "yanlış alarm" demek için acele etme. Root'a doğrudan güvenmeyen, yalnızca eski root'u bilen istemciler zinciri cross certificate üzerinden kurar ve iki gün sonra kırılır. 2021'de olan tam olarak buydu. Let's Encrypt'in eski root'u, IdenTrust'ın DST Root CA X3'ü, 30 Eylül 2021'de sona erdi. Sunucuların çoğu zincirde ISRG (Let's Encrypt'i işleten kâr amacı gütmeyen kuruluş) Root X1'in bu eski root tarafından imzalanmış cross-sign'ını da gönderiyordu. Modern istemciler kısa yolu seçti, ama OpenSSL 1.0.2 gibi istemciler gönderilen uzun zinciri tercih edip süresi dolmuş root'a takıldı. Let's Encrypt cross-sign'ı varsayılan zincirden 8 Şubat 2024'te çıkardı ve cross-sign 30 Eylül 2024'te sona erdi; en çok eski Android cihazlar etkilendi. Alarm için last_chain kullan. earliest'i ise "sunucu gereksiz ya da bitmek üzere olan bir certificate gönderiyor" diye düşük öncelikli bir bilgi olarak izle.

Alert Kuralları ve promtool ile Test

Metric'i alarma çeviren kurallar şöyle:

groups:
  - name: tls-certificates
    rules:
      - alert: TLSProbeFailing
        expr: probe_success{job="blackbox-tls"} == 0
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "TLS probe failing for {{ $labels.instance }} (chain, name or connection)"
      - alert: TLSCertificateExpiringSoon
        expr: (probe_ssl_last_chain_expiry_timestamp_seconds{job="blackbox-tls"} - time()) / 86400 < 10
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.instance }} certificate expires in {{ $value | humanize }} days; renewal did not happen"
      - alert: TLSCertificateExpiryCritical
        expr: (probe_ssl_last_chain_expiry_timestamp_seconds{job="blackbox-tls"} - time()) / 86400 < 3
        for: 15m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.instance }} certificate expires in {{ $value | humanize }} days"
      - alert: TLSServedChainHasExpiringExtraCertificate
        expr: |
          (probe_ssl_earliest_cert_expiry{job="blackbox-tls"} - time()) / 86400 < 10
          and (probe_ssl_last_chain_expiry_timestamp_seconds{job="blackbox-tls"} - time()) / 86400 >= 10
        for: 1h
        labels:
          severity: info
        annotations:
          summary: "{{ $labels.instance }} sends a certificate outside the verified chain that expires soon"

Eşiklerin mantığı otomasyona dayanıyor. 47 günlük bir certificate, ARI (CA'nın sertifikanın ne zaman yenilenmesi gerektiğini bildirmesi) yoksa ömrünün üçte biri kaldığında, yani yaklaşık 16 gün kala yenilenir. 10 günün altına düşmüşse yenileme en az altı gündür başarısız oluyor demektir. Bu, bir insanın bakması gereken bir durum ama henüz yangın değil. 3 gün ise yangın. Kuralları üretmeden önce promtool ile birim testi yazdım. Senaryolar labdakilerin aynısı: yenilemesi takılmış www, zincirinde bitmek üzere olan bir cross certificate taşıyan api, SAN'ı yanlış olan shop:

rule_files:
  - tls-alerts.yml
evaluation_interval: 1m
tests:
  - interval: 1m
    input_series:
      # www: yenileme takılmış, 9 gün kaldı
      - series: 'probe_ssl_last_chain_expiry_timestamp_seconds{job="blackbox-tls",instance="https://www.bookstore.example"}'
        values: '777600x180'
      - series: 'probe_ssl_earliest_cert_expiry{job="blackbox-tls",instance="https://www.bookstore.example"}'
        values: '777600x180'
      # api: zincir 46 gün geçerli, ama fazladan gönderilen cross certificate 2 gün sonra bitiyor
      - series: 'probe_ssl_last_chain_expiry_timestamp_seconds{job="blackbox-tls",instance="https://api.bookstore.example"}'
        values: '3974400x180'
      - series: 'probe_ssl_earliest_cert_expiry{job="blackbox-tls",instance="https://api.bookstore.example"}'
        values: '172800x180'
      # shop: SAN yanlış, probe başarısız
      - series: 'probe_success{job="blackbox-tls",instance="https://shop.bookstore.example"}'
        values: '0x180'
    alert_rule_test:
      - eval_time: 2h
        alertname: TLSCertificateExpiringSoon
        exp_alerts:
          - exp_labels:
              severity: warning
              job: blackbox-tls
              instance: https://www.bookstore.example
            exp_annotations:
              summary: "https://www.bookstore.example certificate expires in 8.917 days; renewal did not happen"
      - eval_time: 2h
        alertname: TLSCertificateExpiryCritical
        exp_alerts: []
      - eval_time: 2h
        alertname: TLSServedChainHasExpiringExtraCertificate
        exp_alerts:
          - exp_labels:
              severity: info
              job: blackbox-tls
              instance: https://api.bookstore.example
            exp_annotations:
              summary: "https://api.bookstore.example sends a certificate outside the verified chain that expires soon"
      - eval_time: 15m
        alertname: TLSProbeFailing
        exp_alerts:
          - exp_labels:
              severity: critical
              job: blackbox-tls
              instance: https://shop.bookstore.example
            exp_annotations:
              summary: "TLS probe failing for https://shop.bookstore.example (chain, name or connection)"
$ promtool check rules tls-alerts.yml
Checking tls-alerts.yml
  SUCCESS: 4 rules found

$ promtool test rules tls-alerts.test.yml
Processing test file: tls-alerts.test.yml
  SUCCESS

Testte zaman sıfırdan başlıyor, bu yüzden 777600 "dokuz gün sonra bitiyor" demek. Sabit eşiklerin bir sınırı var: 6 günlük certificate'larda 10 günlük eşik sürekli alarm verir. Kısa ömürlü certificate'ları ayrı bir job'da veya bir label ile ayır ve eşiği ömre göre seç; örneğin 6 günlükler için 2 gün. Alarmların yönlendirilmesi, susturulması ve runbook bağlantıları Observability serisinin alerting yazısının konusu.

Dışarıdan ölçüm sonucu söyler; sebebi ACME client'ının kendisi bilir. Onu da alarma bağla. cert-manager'da beşinci yazıda gördüğümüz certmanager_certificate_ready_status{condition="False"} metric'i, Caddy'de tls.obtain logger'ının could not get certificate from issuer hata satırı, Certbot'ta ise systemd unit'inin failed durumu. Böylece certificate'ın bitmesine 10 gün kala değil, ilk başarısız denemenin ertesi günü haberin olur.

TLS Config'ini Test Etmek: testssl.sh ve Mozilla Profilleri

Certificate geçerli olsa da TLS ayarları eskimiş olabilir. SSL Labs gibi online tarayıcılar yalnızca internetten erişilen sunucuları tarar. testssl.sh ise aynı kontrolleri iç ağdaki bir sunucuya karşı da yapıyor ve CI'a konabiliyor. Bir eski sunucudan kopyalanmış bu config'i taradım:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5:@SECLEVEL=0;
$ docker run --rm --network tlsseries-net8 ghcr.io/testssl/testssl.sh:3.2 --quiet --color 0 -p -s www.bookstore.example:443
 SSLv2      not offered (OK)
 SSLv3      not offered (OK)
 TLS 1      offered (deprecated)
 TLS 1.1    offered (deprecated)
 TLS 1.2    offered (OK)
 TLS 1.3    offered (OK): final
 NULL ciphers (no encryption)                      not offered (OK)
 Anonymous NULL Ciphers (no authentication)        not offered (OK)
 Export ciphers (w/o ADH+NULL)                     not offered (OK)
 LOW: 64 Bit + DES, RC[2,4], MD5 (w/o export)      not offered (OK)
 Triple DES Ciphers / IDEA                         not offered
 Obsoleted CBC ciphers (AES, ARIA etc.)            offered
 Strong encryption (AEAD ciphers) with no FS       not offered
 Forward Secrecy strong encryption (AEAD ciphers)  offered (OK)

TLS 1.0 ve 1.1 açık. İkisi de RFC 8996 ile 2021'de resmen kullanımdan kaldırıldı ve modern tarayıcılar artık bunlarla bağlanmıyor. @SECLEVEL=0 de bir işaret: OpenSSL 3, TLS 1.0'ı varsayılan güvenlik seviyesinde zaten reddediyor; config onu zorla açıyor. Mozilla'nın SSL Configuration Generator'ı hazır profiller sunuyor. Güncel yönergeler (sürüm 6.0) yalnızca iki profil içeriyor: modern (yalnızca TLS 1.3) ve intermediate (TLS 1.2 ve 1.3). Eskiden olan old profili artık yok. Intermediate profili, yönergelerin JSON (JavaScript Object Notation) çıktısındaki değerlerle yazdım:

# Mozilla "intermediate" (yönergeler 6.0)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
$ docker run --rm --network tlsseries-net8 ghcr.io/testssl/testssl.sh:3.2 --quiet --color 0 -p -s -f www.bookstore.example:443
 TLS 1      not offered
 TLS 1.1    not offered
 TLS 1.2    offered (OK)
 TLS 1.3    offered (OK): final
 Obsoleted CBC ciphers (AES, ARIA etc.)            not offered
 Forward Secrecy strong encryption (AEAD ciphers)  offered (OK)
 KEMs offered                 X25519MLKEM768
 Elliptic curves offered:     prime256v1 secp384r1 X25519 

(Çıktıdan değişen ve yeni eklenen satırları bıraktım.) TLS 1.0/1.1 ve CBC (blok şifrelemede her bloğu bir öncekine bağlayan mod) cipher'ları kapandı, post-quantum hybrid key exchange (X25519MLKEM768) açıldı. Intermediate profilinin desteklediği en eski istemciler listesinde Android 4.4.2, Windows 10'da Internet Explorer 11 ve Java 8u161 var; bunlardan daha eski bir istemcin yoksa doğru seçim bu. Nginx'te HTTPS'in geri kalanı Web Server'lar ve Hosting serisinin "Nginx'te HTTPS, Security Header'ları, Rate Limiting ve Load Balancing" yazısında.

testssl.sh certificate'a da bakıyor. Lab root'unu ekleyip -S ile çalıştırınca şu satırlar dikkat çekiyor:

 Chain of trust               Ok
 Certificate Validity (UTC)   expires < 60 days (46) (2026-11-13 23:01 --> 2026-12-30 23:01)
 Certificate Revocation List  --
 OCSP URI                     --
                              NOT ok -- neither CRL nor OCSP URI provided

"expires < 60 days" uyarısı, 47 günlük certificate döneminde her certificate için çıkacak. Araçların eşikleri yıllık certificate'lar için konmuş; kendi eşiğini ömre göre seç. CRL (iptal edilmiş sertifikaların listesi) veya OCSP (sertifikanın iptal durumunu anlık sorgulama protokolü) adresi olmaması labın test CA'sından geliyor. Public bir certificate'ta bu satırı görürsen CA'nın bilgilerine bak; ikinci yazıda gördüğümüz gibi Let's Encrypt artık yalnızca CRL veriyor.

Gerçek Olay Türleri ve Kısa Bir Runbook

Yenilendi ama Reload Edilmedi

Önce Tahmin Et'in birinci olayı. Nginx eski certificate'la çalışırken yenileme script'i yeni dosyaları yerine koydu, ama Nginx'e reload göndermedi:

$ openssl x509 -in live/fullchain.pem -noout -serial -enddate
serial=073426772D270B25BA70E17801BEFDDB98387BEA
notAfter=Dec 30 23:01:55 2026 GMT
$ ./probe.sh https://www.bookstore.example | grep -E "earliest|serialnumber" | sed -E 's/fingerprint_sha256="[^"]*",//'
probe_ssl_earliest_cert_expiry 1.795474915e+09
probe_ssl_last_chain_info{issuer="CN=Bookstore Lab Issuing CA",serialnumber="073426772d270b25ba70e17801befddb98387be9",subject="CN=www.bookstore.example",subjectalternative="www.bookstore.example,bookstore.example"} 1
$ nginx -s reload
$ ./probe.sh https://www.bookstore.example | grep -E "earliest|serialnumber" | sed -E 's/fingerprint_sha256="[^"]*",//'
probe_ssl_earliest_cert_expiry 1.798671715e+09
probe_ssl_last_chain_info{issuer="CN=Bookstore Lab Issuing CA",serialnumber="073426772d270b25ba70e17801befddb98387bea",subject="CN=www.bookstore.example",subjectalternative="www.bookstore.example,bookstore.example"} 1

Diskte ...BEA, sunulan ...BE9. Yenileme başarılı; Nginx certificate'ı yalnızca başlangıçta ve reload'da okuyor. Cevap: certificate yenilenmiş, ama kimse sunucuya söylememiş. Üçüncü yazıdaki --deploy-hook tam olarak bunun için. Kestrel'in kendi dosya izleyicisi olsa bile, dördüncü yazıda gördüğümüz gibi o da yalnızca Path'i izliyor. Bu olayı sunulan seri numarasıyla diskteki seri numarasını karşılaştırarak ya da sadece expiry alarmıyla yakalarsın.

Eksik Intermediate ve Yanlış SAN

Sunucu yalnızca leaf'i gönderince ve aynı certificate başka bir isimle sunulunca:

$ ./probe.sh https://www.bookstore.example
probe_success 0
... err="Get \"https://172.20.0.2\": tls: failed to verify certificate: x509: certificate signed by unknown authority"
$ ./probe.sh https://shop.bookstore.example
probe_success 0
... err="Get \"https://172.20.0.2\": tls: failed to verify certificate: x509: certificate is valid for www.bookstore.example, bookstore.example, not shop.books

(Hata satırları exporter'ın debug=true çıktısından, kısaltılmış.) Birinci yazıda eksik intermediate'ın masaüstü tarayıcıda çalışıp curl'de kırıldığını görmüştük; Go'nun TLS istemcisi de AIA (sertifikada intermediate sertifikanın adresini veren alan) ile eksik intermediate'ı indirmiyor, bu yüzden blackbox exporter bu hatayı tarayıcılardan önce yakalıyor. İki durumda da expiry metric'i yok; yalnızca TLSProbeFailing alarmı çalar. Yeni bir isim eklendiğinde (bir shop alt domain'i) certificate'a eklenmemesi en sık görülen deploy hatalarından biri.

Root Expiry ve Saat Kayması

Root expiry'yi yukarıda DST Root CA X3 ile gördük. Saat kayması kısa ömürlü certificate'larla daha önemli hale geliyor. 6 günlük, yeni alınmış bir certificate'ı saati iki gün geri kalmış ve dokuz gün ileri gitmiş bir istemcinin gözünden doğruladım:

$ openssl x509 -in shortlived.crt -noout -startdate -enddate
notBefore=Nov 13 23:03:01 2026 GMT
notAfter=Nov 19 23:03:01 2026 GMT
$ openssl verify -CAfile r.crt -attime $((NOW - 2*86400)) shortlived.crt
CN=www.bookstore.example
error 9 at 0 depth lookup: certificate is not yet valid
error shortlived.crt: verification failed
$ openssl verify -CAfile r.crt -attime $((NOW + 7*86400)) shortlived.crt
CN=www.bookstore.example
error 10 at 0 depth lookup: certificate has expired
error shortlived.crt: verification failed

Yıllık certificate'larda iki günlük kayma fark edilmezdi; 6 günlükte ise yenilemeden hemen sonra o istemci bağlanamıyor. Saati senkronize olmayan cihazlar (kiosk'lar, depo tarayıcıları, uzun süre kapalı kalan makineler) ve container'ın içinde saatle oynanan test ortamları bu hatayı üretir. Hata mesajı "certificate is not yet valid" ise önce istemcinin saatine bak.

Runbook

  • Sunulan certificate'ı gör: openssl s_client -connect host:443 -servername host -showcerts ile seri numarası, notAfter, SAN ve gönderilen zincir.
  • Diskteki ile karşılaştır: seri numaraları farklıysa reload veya deploy hook sorunu.
  • Zinciri doğrula: intermediate eksik mi, gereksiz ya da süresi dolmuş bir cross certificate gönderiliyor mu?
  • İsim: istenen host SAN listesinde mi, SNI (TLS handshake'inde istenen host adının bildirilmesi) olmadan hangi certificate dönüyor?
  • ACME client'ının log'una bak: challenge hatası, rate limit, CAA reddi, DNS API'sinin yetkisi.
  • Acil durumda certificate'ı elle al ve kur, ama otomasyonu düzeltmeden olayı kapatma; bir sonraki yenileme aynı yerden kırılır.

Üretimde Dikkat

Lifetime 2029'a kadar 47 güne inerken, yılda bir yapılan bir iş haftalık bir işe dönüşüyor. Aşağıdaki listeyi her certificate için doldurabiliyorsan hazırsın:

  • Otomasyon: her certificate'ı bir araç yeniliyor ve kurulumu (reload, binding, Secret) da otomatik.
  • ARI: client'lar ARI destekliyor; CA erken yenileme isterse kimse sunucuya girmek zorunda kalmıyor.
  • Dışarıdan izleme: her isim için blackbox probe'u, last_chain üzerinde ömre göre eşik, ayrıca probe_success alarmı.
  • ACME hataları: client'ın başarısız denemesi ilk gün alarma dönüşüyor.
  • Pinning yok: certificate'a, key'e veya intermediate'a pinning yapan istemci kalmadı.
  • Envanter ve CT: issuer izin listesi, beklenmedik issuance kontrolü ve güncel CAA kaydı.
  • Saat: sunucularda ve sahadaki cihazlarda NTP (saatleri ağ üzerinden senkronize eden protokol) senkronizasyonu.

İzleme sisteminin kendisini de izle. Blackbox exporter'ın kendi certificate'ı, Prometheus'un hedef listesi ve Alertmanager'ın yönlendirmesi de kırılabilir. Alarm zincirinin uçtan uca çalıştığını, kasıtlı olarak yanlış isimli bir hedefle düzenli olarak test et.

Sık Düşülen Tuzaklar

Diskteki dosyayı izlemek: openssl x509 -enddate'i dosyaya karşı çalıştıran bir cron, reload edilmemiş sunucuyu "sağlıklı" görür. Yukarıdaki ilk olayda diskteki certificate 47 gün geçerliydi. İzleme, istemcinin gördüğünü ölçmeli.

probe_ssl_earliest_cert_expiry'ye alarm koymak: Gereksiz bir cross certificate'ı olan sunucuda sürekli yanlış alarm üretir. Ekip bu alarmı susturmaya alışınca da gerçek alarmı kaçırır. Kritik alarm last_chain'e, earliest düşük öncelikli bilgiye.

Probe başarısızken expiry'ye güvenmek: Zincir veya isim hatasında expiry metric'i hiç üretilmez ve absent olan bir seri expiry alarmını tetiklemez. probe_success == 0 alarmı olmadan, en kötü durumda hiçbir alarm çalmaz.

Yalnızca ana domain'i izlemek: Envanterdeki her isim, iç servisler ve partner endpoint'leri dahil, ayrı bir hedef olmalı. Yedinci yazıdaki partner certificate'ları gibi client certificate'ları blackbox ile ölçülemez; onları issuer tarafında, verildikleri yerde izle.

Eski araç eşiklerini korumak: 30 günlük "expiring soon" eşiği 47 günlük certificate'ta sürekli çalan bir alarma, 6 günlükte ise anlamsız bir alarma dönüşür.

Küçük Bir Deneme

★☆☆ Kendi sitende ./ct-check.sh'ı çalıştır ve CAA kaydına bak. CT'de gördüğün bütün issuer'lar CAA'da izin verilen CA'lar mı?

★★☆ Bu yazıdaki blackbox config'ini ve alert kurallarını bir Prometheus'a bağla, beşinci yazıdaki Caddy ve Pebble kurulumunu hedef yap ve 6 günlük certificate'lar için ayrı bir eşikle bir kural daha yaz. promtool testine 6 günlük bir senaryo ekle.

★★★ Laboratuvarda dört olayı (reload edilmemiş yenileme, eksik intermediate, yanlış SAN, süresi dolmak üzere olan cross certificate) sırayla üret ve her biri için hangi alarmın ne kadar sürede çaldığını ölç. Hangisini hiçbir alarm yakalamadı?

Seri burada bitiyor. Bir certificate'ın içinden başlayıp alınmasına, kurulmasına, otomatik yenilenmesine, HTTPS'in zorunlu kılınmasına, istemci kimliğine ve izlemeye kadar geldik. Bundan sonrası iki komşu seride: certificate'ların dayandığı DNS kayıtları ve CDN için Domain, DNS ve CDN serisi, bu yazıdaki alarmların altyapısı için de Observability serisi.