Let's Encrypt ve ACME ile Otomatik Certificate: Certbot, Nginx, Wildcard ve ARI ile Yenileme
ACME ile certificate almak account, order, authorization, challenge ve finalize adımlarından oluşur; Certbot bunu Nginx'te webroot veya nginx plugin'iyle, wildcard'ı DNS-01 ile yapar ve ARI sayesinde CA'nın önerdiği zamanda yeniler. Pebble üzerinde gerçek koşularla iptal edilen bir certificate'ın ARI ile kendiliğinden yenilenmesini, profilleri, 6 günlük ve IP certificate'larını ve 80 portunu kapatmanın bedelini görüyoruz.
Sürüm notu: Bu yazıdaki örnekler Certbot 5.10.0, Nginx 1.30.6 ve Let's Encrypt'in test sunucusu Pebble (Kasım 2026 image'ı) ile Docker 29 üzerinde denenmiştir; Let's Encrypt'in gerçek directory'si ve profil bilgileri 14 Kasım 2026'da okunmuştur.
Önce Tahmin Et
Bookstore'un Nginx sunucusunda Certbot kurulu ve günde iki kez certbot renew çalışıyor. Certificate'lar 90 günlük ve Certbot onları kalan ömrün üçte biri kaldığında, yani yaklaşık 60. günde yeniliyor. Bir sabah CA'dan (sertifika veren güvenilir kuruluş) duyuru geliyor: doğrulama yazılımlarında bir hata bulunmuş, etkilenen certificate'lar beş gün içinde iptal edilecek. Bu kurgu değil; Let's Encrypt 2020'de CAA (hangi CA'ların sertifika verebileceğini belirten DNS kaydı) kontrolündeki bir hata yüzünden 3.048.289 certificate'ı bu şekilde iptal edeceğini duyurmuştu.
Tahmin et: Bookstore'un certificate'ı alınalı henüz 20 gün olduysa, Certbot onu iptalden önce yeniler mi? Yenilemesi için birinin sunucuya girmesi gerekir mi? Cevabı yazının ortasında, certificate'ı gerçekten iptal edip izleyeceğiz.
ACME Akışı: Account, Order, Authorization, Challenge, Finalize
ACME (Automatic Certificate Management Environment; RFC 8555, yani IETF'nin, Internet Engineering Task Force'un Request for Comments dokümanı), certificate almayı bir HTTP API'sine dönüştüren protokol. Her şey bir directory adresiyle başlar. Let's Encrypt'in production directory'si bugün şöyle:
$ curl -s https://acme-v02.api.letsencrypt.org/directory
{
"keyChange": "https://acme-v02.api.letsencrypt.org/acme/key-change",
"meta": {
"caaIdentities": [
"letsencrypt.org"
],
"profiles": {
"classic": "https://letsencrypt.org/docs/profiles#classic",
"shortlived": "https://letsencrypt.org/docs/profiles#shortlived",
"tlsserver": "https://letsencrypt.org/docs/profiles#tlsserver"
},
"termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.8-July-06-2026.pdf",
"website": "https://letsencrypt.org"
},
"newAccount": "https://acme-v02.api.letsencrypt.org/acme/new-acct",
"newNonce": "https://acme-v02.api.letsencrypt.org/acme/new-nonce",
"newOrder": "https://acme-v02.api.letsencrypt.org/acme/new-order",
"nxZYWyxD4ek": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
"renewalInfo": "https://acme-v02.api.letsencrypt.org/acme/renewal-info",
"revokeCert": "https://acme-v02.api.letsencrypt.org/acme/revoke-cert"
}
Client bu adreslerin dışında hiçbir URL varsaymaz. Rastgele isimli nxZYWyxD4ek alanı da bunu sınayan bir şaka: directory'yi sabit bir şema gibi parse eden client'lar kırılsın diye konmuş. Akışın adımları:
- Account: Client bir key pair üretir ve
newAccountile kaydolur. Sonraki her istek bu account key'iyle imzalanır. - Order:
newOrderile "şu isimler için certificate istiyorum" denir; CA her isim için bir authorization döner. - Authorization ve challenge: Her authorization'da HTTP-01, DNS-01 (DNS: Domain Name System) gibi challenge seçenekleri vardır. Client birini hazırlar ve CA'ya "hazırım" der, CA doğrular.
- Finalize: Bütün authorization'lar geçerliyse client CSR'ı (certificate imzalama isteği) gönderir, CA certificate'ı imzalar ve client onu indirir.
Bu yazının laboratuvarında Let's Encrypt yerine onun açık kaynak test sunucusu Pebble'ı kullandım. Pebble aynı protokolü konuşuyor ama gerçek bir CA değil; her başlatıldığında yeni bir root üretiyor. Bu yüzden komutlarda --server https://pebble:14000/dir görüyorsun. Kendi sunucunda bu parametreyi yazmazsan Certbot Let's Encrypt production'a, --test-cert ile staging'e gider. Certbot'un debug log'u akışı birebir gösteriyor:
$ grep -oE "Sending (GET|POST|HEAD) request to [^ ]+" /var/log/letsencrypt/letsencrypt.log
Sending GET request to https://pebble:14000/dir.
Sending HEAD request to https://pebble:14000/nonce-plz.
Sending POST request to https://pebble:14000/sign-me-up:
Sending POST request to https://pebble:14000/order-plz:
Sending POST request to https://pebble:14000/authZ/Ch3IeZwNYMkKOroGEmMcWYznO264tUdS05S53mYBkxs:
Sending POST request to https://pebble:14000/authZ/ZpA_NHihl-1tMJkTKcpSDBu-aHdvRAlTtBTFlauuf4E:
Sending POST request to https://pebble:14000/chalZ/xskVwxPNsj8vPPk4KXBfUXlx8TgOa3eEvB6OawbrjIA:
Sending POST request to https://pebble:14000/chalZ/y3JuOtCXCriGNZjNdDYgZncLK-qJUBx9zZg7Gpewszc:
Sending POST request to https://pebble:14000/authZ/Ch3IeZwNYMkKOroGEmMcWYznO264tUdS05S53mYBkxs:
Sending POST request to https://pebble:14000/authZ/ZpA_NHihl-1tMJkTKcpSDBu-aHdvRAlTtBTFlauuf4E:
Sending POST request to https://pebble:14000/finalize-order/ck7m5TpPlkSsgW7qMnUQR4gd2w54QCvNcqwAOLkC_B4:
Sending POST request to https://pebble:14000/my-order/ck7m5TpPlkSsgW7qMnUQR4gd2w54QCvNcqwAOLkC_B4:
Sending POST request to https://pebble:14000/certZ/424b9a33d3d87124:
Directory, nonce, account, order, iki isim için iki authorization, iki challenge, authorization'ların sonucunu sorgulama, finalize, order'ın durumunu sorgulama ve certificate'ı indirme. Pebble'daki endpoint adları (sign-me-up, order-plz) şakacı, ama sıra Let's Encrypt'tekiyle aynı. Protokol bu sırayı garanti ettiği için Certbot, acme.sh, lego, Caddy veya cert-manager gibi client'lar birbirinin yerine kullanılabiliyor.
Staging ve Rate Limit'ler
Let's Encrypt production'ında rate limit'ler var ve config denemeleri sırasında onlara takılmak kolay. Güncel değerlerden en sık karşılaşılanlar şunlar: kayıtlı domain başına 7 günde 50 certificate, birebir aynı isim kümesi için 7 günde 5 certificate (duplicate limiti) ve isim başına saatte 5 başarısız doğrulama. Yeni bir kurulumu her zaman staging'de (--test-cert) dene. Staging'in limitleri çok daha yüksek; certificate'ları güvenilir olmayan bir root'tan geldiği için tarayıcıda uyarı verir ama akışın tamamını gerçekten çalıştırır. Bir ayrıntı daha önemli: ARI (CA'nın sertifikanın ne zaman yenilenmesi gerektiğini bildirmesi) ile koordine edilen yenilemeler bütün rate limit'lerden muaf. Aynı isim kümesiyle yapılan normal yenilemeler ise yeni order ve domain limitlerinden muaf, duplicate limitinden değil.
Certbot ile Nginx: Webroot
Linux'taki en yaygın kurulum: Nginx önde duruyor, Certbot challenge dosyasını Nginx'in servis ettiği bir klasöre yazıyor. Nginx'in 80 portundaki config'i yalnızca iki iş yapıyor: challenge'ı servis etmek ve kalan her şeyi HTTPS'e (HTTP Secure) redirect etmek:
server {
listen 80 default_server;
server_name bookstore.example www.bookstore.example;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 301 https://$host$request_uri;
}
}
Certificate'ı almak:
$ certbot certonly --webroot -w /var/www/acme \
-d bookstore.example -d www.bookstore.example \
--server https://pebble:14000/dir -m ops@bookstore.example --agree-tos --no-eff-email \
--deploy-hook "nginx -s reload"
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Account registered.
Requesting a certificate for bookstore.example and www.bookstore.example
Hook 'deploy-hook' ran with error output:
2026/11/13 21:39:45 [notice] 69#69: signal process started
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/bookstore.example/fullchain.pem
Key is saved at: /etc/letsencrypt/live/bookstore.example/privkey.pem
This certificate expires on 2027-02-11.
These files will be updated when the certificate renews.
"ran with error output" satırı korkutucu görünüyor ama hook başarılı. nginx -s reload bilgi mesajını stderr'e yazıyor, Certbot da stderr'e yazılan her şeyi bu başlıkla gösteriyor. systemd ile çalışan bir sunucuda systemctl reload nginx kullanırsan bu gürültü olmaz. --deploy-hook yalnızca certificate gerçekten yenilendiğinde çalışır ve renewal config'ine kaydedilir, yani sonraki yenilemelerde de çalışacak.
Doğrulama sırasında Nginx'in access log'una düşen istekler:
$ docker logs --since $T0 tlsseries-web 2>&1 | grep acme-challenge | awk '{print $1, $4, $6, $7, $9}'
172.31.33.20 [13/Nov/2026:21:39:43 "GET /.well-known/acme-challenge/fjvv7XptyCayJkVnJbHnCH_IhLXklC_5oo3DSeq4LKM 200
172.31.33.20 [13/Nov/2026:21:39:43 "GET /.well-known/acme-challenge/fjvv7XptyCayJkVnJbHnCH_IhLXklC_5oo3DSeq4LKM 200
172.31.33.20 [13/Nov/2026:21:39:43 "GET /.well-known/acme-challenge/yWBInESu70B2h2eERCK7qOKDXzwyWB10CMcWrVuXmbs 200
172.31.33.20 [13/Nov/2026:21:39:43 "GET /.well-known/acme-challenge/yWBInESu70B2h2eERCK7qOKDXzwyWB10CMcWrVuXmbs 200
172.31.33.20 [13/Nov/2026:21:39:43 "GET /.well-known/acme-challenge/fjvv7XptyCayJkVnJbHnCH_IhLXklC_5oo3DSeq4LKM 200
172.31.33.20 [13/Nov/2026:21:39:43 "GET /.well-known/acme-challenge/yWBInESu70B2h2eERCK7qOKDXzwyWB10CMcWrVuXmbs 200
Pebble her token'ı üç kez istedi. Let's Encrypt de doğrulamayı internetin farklı noktalarından tekrarlar; bu yüzden 80 portunu yalnızca "CA'nın IP adresine" açmaya çalışmak işe yaramaz. Certbot'un bıraktığı dosya düzeni:
$ cd /etc/letsencrypt/live/bookstore.example && ls -l | awk 'NR>1 {print $9, $10, $11}'
README
cert.pem -> ../../archive/bookstore.example/cert1.pem
chain.pem -> ../../archive/bookstore.example/chain1.pem
fullchain.pem -> ../../archive/bookstore.example/fullchain1.pem
privkey.pem -> ../../archive/bookstore.example/privkey1.pem
live/ altındakiler symlink; asıl dosyalar archive/ altında ve her yenilemede numara artıyor. Nginx'e her zaman live/ yolunu ver, böylece yenilemede config değişmez:
server {
listen 443 ssl;
server_name bookstore.example www.bookstore.example;
ssl_certificate /etc/letsencrypt/live/bookstore.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bookstore.example/privkey.pem;
location / {
return 200 "Bookstore over HTTPS\n";
}
}
$ curl -sS --cacert /tmp/pebble-root.pem https://bookstore.example/
Bookstore over HTTPS
$ echo | openssl s_client -connect bookstore.example:443 -servername bookstore.example \
-CAfile /tmp/pebble-root.pem 2>/dev/null | grep -E '^ *[0-9] s:|^ i:|^Verify'
0 s:
i:CN=Pebble Intermediate CA 402e82
1 s:CN=Pebble Intermediate CA 402e82
i:CN=Pebble Root CA 61303e
Verify return code: 0 (ok)
Leaf'in Subject'i boş; Pebble, Let's Encrypt'in tlsserver profili gibi CN (sertifikadaki sahip adı alanı) koymuyor, isimler yalnızca SAN'da (sertifikanın geçerli olduğu alan adları listesi). Serinin ilk yazısında anlattığımız fullchain kuralı burada kendiliğinden sağlanıyor.
Webroot mu, nginx Plugin'i mi?
Certbot'un nginx plugin'i (--nginx), challenge'ı geçici bir config ile kendisi çözer ve certificate'ı config'ine kendisi yazar. Bookstore'un API'si için ayrı bir server block'u üzerinde denedim:
$ certbot --nginx -d api.bookstore.example --redirect -n --server https://pebble:14000/dir
Requesting a certificate for api.bookstore.example
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/api.bookstore.example/fullchain.pem
Key is saved at: /etc/letsencrypt/live/api.bookstore.example/privkey.pem
This certificate expires on 2027-02-11.
These files will be updated when the certificate renews.
Deploying certificate
Successfully deployed certificate for api.bookstore.example to /etc/nginx/conf.d/api.conf
Congratulations! You have successfully enabled HTTPS on https://api.bookstore.example
$ diff -u /tmp/api.conf.orig /etc/nginx/conf.d/api.conf
@@ -1,8 +1,26 @@
server {
- listen 80;
server_name api.bookstore.example;
location / {
proxy_pass http://127.0.0.1:8080;
}
+
+ listen 443 ssl; # managed by Certbot
+ ssl_certificate /etc/letsencrypt/live/api.bookstore.example/fullchain.pem; # managed by Certbot
+ ssl_certificate_key /etc/letsencrypt/live/api.bookstore.example/privkey.pem; # managed by Certbot
+ include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
+ ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
+
}
+server {
+ if ($host = api.bookstore.example) {
+ return 301 https://$host$request_uri;
+ } # managed by Certbot
+
+
+ listen 80;
+ server_name api.bookstore.example;
+ return 404; # managed by Certbot
+
+
+}
Tek komutla HTTPS açıldı, ama Certbot senin dosyanı yeniden yazdı: listen 80'i taşıdı, kendi TLS ayar dosyasını include etti ve bir redirect block'u ekledi. Config'i Git'te tutuyorsan veya Ansible gibi bir araçla üretiyorsan, bir sonraki deploy bu değişiklikleri siler. Kural basit: tek sunuculu, elle yönetilen bir makinede --nginx rahat; config as code yapıyorsan certonly --webroot ile certificate'ı al, Nginx config'ini kendin yaz. Bu yazının geri kalanında webroot ile devam ediyoruz. Nginx'in HTTPS config'inin kendisi (protokoller, header'lar, HTTP/2) Web Server'lar ve Hosting serisinin "Nginx'te HTTPS, Security Header'ları, Rate Limiting ve Load Balancing" yazısında.
Yenileme: systemd Timer, --dry-run ve ARI
Certbot'u pip veya kaynak paketlerden kurduysan yenilemeyi zamanlamak senin işin; dağıtım paketleri ve snap bunu kendileri kurar. systemd timer'ı:
# /etc/systemd/system/certbot-renew.service
[Unit]
Description=Renew certificates with Certbot
[Service]
Type=oneshot
ExecStart=/opt/certbot/bin/certbot renew --quiet
# /etc/systemd/system/certbot-renew.timer
[Unit]
Description=Run certbot renew twice a day
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
$ systemd-analyze verify certbot-renew.timer certbot-renew.service; echo "verify exit=$?"
verify exit=0
$ systemd-analyze calendar --iterations=3 "*-*-* 00,12:00:00"
Normalized form: *-*-* 00,12:00:00
Next elapse: Sat 2026-11-14 00:00:00 UTC
From now: 2h 16min left
Iteration #2: Sat 2026-11-14 12:00:00 UTC
From now: 14h left
Iteration #3: Sun 2026-11-15 00:00:00 UTC
From now: 1 day 2h left
Günde iki kez çalışmak fazla değil: certbot renew yenileme zamanı gelmemiş certificate'lara dokunmaz. RandomizedDelaySec, binlerce sunucunun aynı saniyede CA'ya yüklenmesini önler. Certbot de etkileşimsiz çalıştığında kendi rastgele beklemesini ekliyor; log'da Non-interactive renewal: random delay of 263.347987354948 seconds gibi bir satır gördüm. Persistent=true ise makine kapalıyken kaçan çalışmayı açılışta telafi eder. Container'ımda systemd çalışmadığı için timer'ı gerçekten tetikleyemedim; dosyaları systemd-analyze ile doğruladım.
Yenileme zincirini, gerçek bir yenileme beklemeden test etmek için:
$ certbot renew --dry-run --server https://pebble:14000/dir
Processing /etc/letsencrypt/renewal/bookstore.example.conf
Simulating renewal of an existing certificate for bookstore.example and www.bookstore.example
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/bookstore.example/fullchain.pem (success)
Laboratuvarda --server ekledim, çünkü --dry-run varsayılan olarak Let's Encrypt staging'ine gidiyor. İlk denemede --server yazmayı unuttuğumda Certbot acme-staging-v02.api.letsencrypt.org'a bağlanmaya çalıştı. Production sunucunda bu iyi bir şey: dry-run production rate limit'lerini harcamaz. Dikkat edilmesi gereken bir ayrıntı daha var: dry-run deploy hook'larını çalıştırmaz. Hook'un kendisini test etmek istiyorsan --run-deploy-hooks ekle.
Şimdi ARI. RFC 9773 ile standartlaşan ARI, CA'ya "bu certificate'ı ne zaman yenilemeliyim?" diye sormayı sağlar. Certbot 4.1'den beri certbot renew her çalışmada bunu soruyor. Soru, certificate'ı imzalayan CA'nın key identifier'ı (AKI, Authority Key Identifier) ile certificate'ın seri numarasından oluşan bir kimlikle soruluyor:
$ aki=$(openssl x509 -in cert.pem -noout -ext authorityKeyIdentifier | tail -1 | tr -d ' :')
$ serial=$(openssl x509 -in cert.pem -noout -serial | cut -d= -f2)
$ b64url() { xxd -r -p | base64 | tr '+/' '-_' | tr -d '='; }
$ echo "$(echo $aki | b64url).$(echo $serial | b64url)"
AvON4hzT-xLJspVeKzVX0-KAMDM.QkuaM9PYcSQ
$ curl -s -D - --cacert /pebble.minica.pem \
https://pebble:14000/draft-ietf-acme-ari-03/renewalInfo/AvON4hzT-xLJspVeKzVX0-KAMDM.QkuaM9PYcSQ \
| grep -iE "retry-after|start|end|^\{|\}"
retry-after: 21600
{
"start": "2027-01-11T21:39:43Z",
"end": "2027-01-13T21:39:43Z"
}
}
CA, 90 günlük certificate için 59. ile 61. günler arasını öneriyor; retry-after ise "altı saat sonra tekrar sor" diyor. Certbot bunu renewal config'ine kaydediyor (ari_retry_after = 2026-11-14T03:40:03) ve o saate kadar CA'yı rahatsız etmiyor:
$ certbot renew
Processing /etc/letsencrypt/renewal/bookstore.example.conf
Certificate not yet due for renewal
The following certificates are not due for renewal yet:
/etc/letsencrypt/live/bookstore.example/fullchain.pem expires on 2027-02-11 (skipped)
No renewals were attempted.
Önce Tahmin Et'teki senaryoyu şimdi kuralım. Certificate'ı CA tarafında iptal ediyorum ve ARI'ye tekrar soruyorum:
$ certbot revoke --cert-name bookstore.example --reason keycompromise --no-delete-after-revoke --non-interactive
Congratulations! You have successfully revoked the certificate that was located at /etc/letsencrypt/live/bookstore.example/cert.pem.
$ curl -s --cacert /pebble.minica.pem \
https://pebble:14000/draft-ietf-acme-ari-03/renewalInfo/AvON4hzT-xLJspVeKzVX0-KAMDM.QkuaM9PYcSQ
{
"suggestedWindow": {
"start": "2026-11-13T20:44:52.174Z",
"end": "2026-11-13T20:44:53.174Z"
}
}
Önerilen pencere artık geçmişte. CA'nın dilinde bunun anlamı "hemen yenile". Gerçek hayatta bir sonraki certbot renew en geç altı saat sonra bu cevabı görür. Laboratuvarda beklememek için renewal config'indeki ari_retry_after satırını sildim, sonra yeniledim:
$ certbot renew
Processing /etc/letsencrypt/renewal/bookstore.example.conf
Renewing an existing certificate for bookstore.example and www.bookstore.example
Hook 'deploy-hook' ran with error output:
2026/11/13 21:48:34 [notice] 515#515: signal process started
Congratulations, all renewals succeeded:
/etc/letsencrypt/live/bookstore.example/fullchain.pem (success)
$ grep -h -E "Certificate is due for renewal|random delay" /var/log/letsencrypt/letsencrypt.log | cut -d: -f4-
INFO:certbot._internal.renewal:Certificate is due for renewal, auto-renewing...
INFO:certbot._internal.renewal:Non-interactive renewal: random delay of 220.4705418133215 seconds
$ cd /etc/letsencrypt/live/bookstore.example && ls -l | awk 'NR>1 {print $9, $10, $11}'
README
cert.pem -> ../../archive/bookstore.example/cert2.pem
chain.pem -> ../../archive/bookstore.example/chain2.pem
fullchain.pem -> ../../archive/bookstore.example/fullchain2.pem
privkey.pem -> ../../archive/bookstore.example/privkey2.pem
Önce Tahmin Et'in cevabı bu. Henüz 20 günlük bir certificate için ARI olmadan Certbot 60. günü beklerdi; ARI ile CA "şimdi" deyince timer'ın bir sonraki çalışmasında yeniledi, deploy hook Nginx'i reload etti ve kimsenin sunucuya girmesi gerekmedi. ARI'nin değeri tam olarak bu: yenileme zamanını sabit bir kuraldan alıp, olay anında durumu bilen CA'ya devretmek. Laboratuvarda bir şeyi daha gördüm: Certbot 5.10'un yenilemede gönderdiği order'da, RFC 9773'teki eski certificate'ı işaret eden replaces alanı yoktu. Let's Encrypt'in ARI rate limit muafiyeti bu alana bağlı. Aynı isim kümesiyle yapılan yenileme yine de yeni order ve domain limitlerinden muaf.
Wildcard için DNS-01
*.bookstore.example gibi bir wildcard yalnızca DNS-01 ile alınabilir. Certbot'un hazır DNS plugin'leri var (Cloudflare, Route 53, Azure DNS ve diğerleri için). Plugin'i olmayan bir sağlayıcıda ise --manual ile iki hook script'i yazarsın. Laboratuvarda DNS sunucusu olarak Pebble'ın test DNS sunucusu pebble-challtestsrv'yi kullandım; hook'lar onun HTTP API'sini çağırıyor:
#!/bin/sh
# /etc/letsencrypt/hooks/dns-auth.sh
# Lab: test DNS sunucusunun API'si (pebble-challtestsrv); production'da DNS sağlayıcının API'si
curl -sf -X POST http://localhost:8055/set-txt \
-d "{\"host\":\"_acme-challenge.${CERTBOT_DOMAIN}.\",\"value\":\"${CERTBOT_VALIDATION}\"}"
#!/bin/sh
# /etc/letsencrypt/hooks/dns-cleanup.sh
curl -sf -X POST http://localhost:8055/clear-txt \
-d "{\"host\":\"_acme-challenge.${CERTBOT_DOMAIN}.\"}"
$ certbot certonly --manual --preferred-challenges dns \
--manual-auth-hook /etc/letsencrypt/hooks/dns-auth.sh \
--manual-cleanup-hook /etc/letsencrypt/hooks/dns-cleanup.sh \
--cert-name bookstore-wildcard -d bookstore.example -d "*.bookstore.example" \
--server https://pebble:14000/dir -n
Requesting a certificate for bookstore.example and *.bookstore.example
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/bookstore-wildcard/fullchain.pem
Key is saved at: /etc/letsencrypt/live/bookstore-wildcard/privkey.pem
This certificate expires on 2027-02-11.
$ openssl x509 -in /etc/letsencrypt/live/bookstore-wildcard/cert.pem -noout -ext subjectAltName
X509v3 Subject Alternative Name: critical
DNS:bookstore.example, DNS:*.bookstore.example
DNS sunucusunun log'u, apex ve wildcard'ın aynı _acme-challenge.bookstore.example ismine iki ayrı TXT değeri yazdığını gösteriyor:
Added TXT response for Host "_acme-challenge.bookstore.example." - Value "XsGkEbEjw6_yjBpoNEBIcclqKB_y638RjDQo8caLstI"
Added TXT response for Host "_acme-challenge.bookstore.example." - Value "5g1NzrmOkh1KKLy8P4hPhxeYDcZYKVDGAvVZ9DPyGzE"
Removed TXT response for Host "_acme-challenge.bookstore.example."
Removed TXT response for Host "_acme-challenge.bookstore.example."
DNS-01'in asıl riski, DNS API credential'ının kendisi. Bu credential'ı ele geçiren biri yalnızca certificate almaz; domain'in bütün kayıtlarını değiştirebilir, mail'ini ve trafiğini başka yere çevirebilir. Üç önlem var. Birincisi, token'ı sağlayıcının izin verdiği en dar yetkiyle oluştur: yalnızca o zone, yalnızca DNS kaydı düzenleme. İkincisi, _acme-challenge.bookstore.example'ı CNAME (bir alan adını başka bir alan adına yönlendiren DNS kaydı) kaydıyla yalnızca doğrulama için kullanılan ayrı bir zone'a delege et. Let's Encrypt bu delegasyonu takip ediyor; böylece otomasyonun yazma yetkisi asıl zone'a hiç dokunmaz. Üçüncüsü, credential dosyasını chmod 600 ile yalnızca root'un okuyabileceği şekilde tut; Certbot DNS plugin'leri izinleri geniş bir dosya için uyarı veriyor.
Ufukta DNS-PERSIST-01 var: bir kere yazılan ve CA ile ACME account'unu adıyla anan kalıcı bir TXT kaydı. Böylece her yenilemede DNS'e yazmak gerekmeyecek. Let's Encrypt, Şubat 2026'daki duyurusunda production'ı 2026'nın ikinci çeyreği için hedeflemişti. Staging'de bir taslak sürümü denendi, ancak Haziran 2026'da Let's Encrypt ekibi IETF taslağındaki açık bir konu çözülene kadar production'a almayacaklarını yazdı. 14 Kasım 2026 itibarıyla Let's Encrypt'in challenge türleri sayfasında yer almıyor. Kullanmayı planlıyorsan önce güncel durumu kontrol et.
Profiller, 6 Günlük ve IP Certificate'ları, Generation Y
Let's Encrypt, Ocak 2025'ten beri ACME profillerini destekliyor. Order'da bir profil adı göndererek certificate'ın biçimini seçiyorsun:
| Profil | Ömür | Subject CN | İsim sayısı | IP adresi | Authorization yeniden kullanımı |
|---|---|---|---|---|---|
classic (varsayılan) | 90 gün | Var | 100 | Hayır | 30 gün |
tlsserver | 45 gün | Yok | 25 | Hayır | 7 saat |
shortlived | 160 saat (yaklaşık 6 gün) | Yok | 25 | Evet | 7 saat |
Certbot 4.0'dan beri iki seçenek var. --preferred-profile, profil yoksa varsayılana düşer. --required-profile ise profil yoksa hata verir. Seçtiğin profil renewal config'ine kaydedilir. Laboratuvardaki Pebble'ı aynı üç profille, aynı sürelerle yeniden başlattım:
$ certbot certonly --webroot -w /var/www/acme --cert-name bookstore-short -d bookstore.example \
--preferred-profile shortlived --server https://pebble:14000/dir -n
Requesting a certificate for bookstore.example
This certificate expires on 2026-11-19.
$ grep profile /etc/letsencrypt/renewal/bookstore-short.conf
preferred_profile = shortlived
$ certbot certonly --webroot -w /var/www/acme --cert-name bookstore-typo -d bookstore.example \
--required-profile tlserver --server https://pebble:14000/dir -n
Requesting a certificate for bookstore.example
An unexpected error occurred:
Order includes unrecognized profile name "tlserver"
İkinci komuttaki yazım hatası iyi bir ders: --required-profile yanlış bir profil adını sessizce geçiştirmez. --preferred-profile ise Let's Encrypt bir profili kaldırdığında yenilemenin kırılmamasını sağlar. Production'da ikincisini kullan.
Ocak 2026'dan beri genel kullanımda olan IP adresi certificate'ları da shortlived profiline bağlı. Let's Encrypt, IP adreslerinin domain'lerden daha sık el değiştirdiği gerekçesiyle IP certificate'ını yalnızca kısa ömürlü veriyor. Certbot 5.3 ile --ip-address parametresi geldi, 5.4 ile webroot da destekliyor. IP adresi için DNS-01 kullanılamaz; HTTP-01 veya TLS-ALPN-01 gerekir:
$ certbot certonly --webroot -w /var/www/acme --cert-name bookstore-ip --ip-address 172.31.33.10 \
--preferred-profile shortlived --server https://pebble:14000/dir -n
Requesting a certificate for 172.31.33.10
This certificate expires on 2026-11-19.
$ openssl x509 -in /etc/letsencrypt/live/bookstore-ip/cert.pem -noout -ext subjectAltName -dates
X509v3 Subject Alternative Name: critical
IP Address:172.31.33.10
notBefore=Nov 13 21:50:04 2026 GMT
notAfter=Nov 19 21:50:03 2026 GMT
6 günlük bir certificate'ı Certbot, ömrünün yarısı kaldığında yeniliyor (10 günden kısa certificate'lar için kural bu). Yani günde iki kez çalışan timer burada şart. Sistemin bir gün bile durması certificate'ı riske atar.
2026'da iki değişiklik daha oldu. Birincisi Generation Y hiyerarşisi: varsayılan profil Mayıs 2026'dan beri yeni Root YE ve Root YR altındaki intermediate'lardan imzalıyor. Serinin ilk yazısında gördüğümüz cross-sign'lı uzun zincir buradan geliyor ve eski trust store'larla uyumu cross-sign sağlıyor. İkincisi, TLS client authentication EKU'sunun (sertifikanın hangi amaçla kullanılabileceğini belirten alan) kaldırılması: Let's Encrypt 11 Şubat 2026'dan beri varsayılan profilde bu EKU'yu koymuyor, geçiş için sunulan tlsclient profili de 8 Temmuz 2026'da kapandı. Let's Encrypt certificate'ını mTLS'te (mutual TLS) client certificate olarak kullanan sistemler, yenilemede yeni certificate'ın client authentication için geçersiz olduğunu görür. Bu ihtiyacın doğru adresi internal bir CA ve serinin yedinci yazısının konusu.
Üretimde Dikkat
Yenilemeyi izle, sadece kurmakla yetinme. Timer'ın varlığı yenilemenin çalıştığını kanıtlamaz. Sunucunun gerçekten sunduğu certificate'ın kalan gününü dışarıdan ölçen bir alarm olmalı; bunu serinin son yazısında kuracağız. Let's Encrypt, Haziran 2025'te expiry e-postalarını tamamen kapattı, yani artık seni CA da uyarmayacak.
Deploy hook'u test et. Yeni certificate diske yazılıp Nginx reload edilmezse sunucu eski certificate'ı sunmaya devam eder ve süresi dolar. Bu, sektördeki en yaygın "yenilendi ama kesinti oldu" olayı. certbot renew --dry-run --run-deploy-hooks ile hook'un çalıştığını, ardından openssl s_client ile sunulan seri numarasının değiştiğini doğrula.
/etc/letsencrypt'i yedekle ve taşınabilir tut. Account key'i, renewal config'leri ve certificate'lar burada. Sunucu yeniden kurulduğunda bu klasör yoksa yeni account açarsın; bu sorun değil, ama hook'lar ve profil seçimleri de kaybolur.
CAA kaydı koy. DNS'te CAA 0 issue "letsencrypt.org" kaydı, domain'in için yalnızca Let's Encrypt'in certificate verebileceğini söyler. Ayrıntıları Domain, DNS ve CDN serisinde.
Sık Düşülen Tuzaklar
80 portunu kapatmak: "Her şey HTTPS, 80'e gerek yok" diye 80'i firewall'da kapatınca HTTP-01 kırılır. Kapalı bir porta işaret eden bir isimle denedim:
$ certbot certonly --webroot -w /var/www/acme -d shop.bookstore.example --server https://pebble:14000/dir -n
Requesting a certificate for shop.bookstore.example
Certbot failed to authenticate some domains (authenticator: webroot). The Certificate Authority reported these problems:
Identifier: shop.bookstore.example
Type: connection
Detail: Get "http://shop.bookstore.example:80/.well-known/acme-challenge/zlnlY8fFa1osJ61nbhIAVFXI7uILvClI-pmjvaxEXHc": dial tcp 172.31.33.20:80: connect: connection refused
Hint: The Certificate Authority failed to download the temporary challenge files created by Certbot. Ensure that the listed identifiers serve their content from the provided --webroot-path/-w and that files created there can be downloaded from the internet.
Some challenges have failed.
İlk alma işlemi başarılı olmuş, 80 portu sonradan kapatılmış bir sunucuda bu hata 60 gün sonra, yenilemede ortaya çıkar. 80'i açık tut ve yalnızca challenge'ı servis edip gerisini 301 ile redirect et; ya da DNS-01'e geç.
Default server'ı kaybetmek: --nginx plugin'inin eklediği 80 block'u, conf.d altında alfabetik olarak önce gelen bir dosyadaydı. Bu yüzden IP adresiyle gelen challenge isteğinin default server'ı o oldu ve istek 404 aldı. Challenge'ı servis eden block'a listen 80 default_server; yazınca düzeldi.
CDN (statik dosyaları kullanıcıya yakın sunuculardan dağıtan ağ) proxy'si arkasında HTTP-01: Cloudflare gibi bir proxy, domain'in önüne geçtiğinde CA'nın isteği önce proxy'ye gider. Proxy'nin "her şeyi HTTPS'e zorla" kuralı veya kendi edge certificate'ı challenge'ı origin'e ulaştırmayabilir. Ya challenge path'ini proxy kurallarında serbest bırak ya da DNS-01 kullan. Edge ve origin certificate ayrımı beşinci yazının konusu.
Certbot'u container'da volume'süz çalıştırmak: Certificate'lar ve account key'i container'ın kendi dosya sisteminde kalırsa, container silindiğinde hepsi gider ve her çalıştırmada yeni bir certificate istenir. Bu da duplicate limitine (7 günde 5) hızla takılmak demek. /etc/letsencrypt mutlaka kalıcı bir volume olmalı.
Deploy hook'u --pre-hook/--post-hook ile karıştırmak: --post-hook her renew çalışmasından sonra, yenileme olmasa bile çalışır. Günde iki kez gereksiz reload yapar. Sadece yenilemede çalışması gereken iş --deploy-hook'a yazılır.
Küçük Bir Deneme
★☆☆ Kendi bir test sunucunda Certbot'u --test-cert ile Let's Encrypt staging'ine karşı çalıştır. /var/log/letsencrypt/letsencrypt.log'da bu yazıdaki istek sırasını bul ve hangi isteğin hangi ACME adımı olduğunu not et.
★★☆ Bir certificate için ARI kimliğini bu yazıdaki gibi hesapla ve Let's Encrypt'in renewal-info endpoint'ine (staging veya production) sor. Önerilen pencere certificate'ın ömrünün yüzde kaçına denk geliyor? Retry-After kaç saniye?
★★★ Pebble ve pebble-challtestsrv'yi Docker'da ayağa kaldırıp bu yazıdaki iptal deneyini tekrarla, ama bu kez yenilemeyi certbot renew yerine systemd timer'ın tetiklemesini bekle (gerçek bir sanal makinede). Deploy hook'un Nginx'i reload ettiğini ve sunulan seri numarasının değiştiğini dışarıdan doğrula.
Certificate artık diskte ve kendiliğinden yenileniyor. Ama Nginx'in önünde olmadığı durumlar da var: Kestrel'in TLS'i kendisinin sonlandırdığı bir container, geliştirici makinesindeki dev certificate veya internal bir servise bağlanan HttpClient. Bir sonraki yazıda .NET tarafına geçiyoruz: dev-certs, PEM (sertifika ve anahtarlar için Base64 metin formatı) ve PFX (PKCS #12) yükleme, SNI (TLS handshake'inde istenen host adının bildirilmesi), yenilemeden sonra restart'sız reload ve HttpClient'ın certificate doğrulaması.