Sertifika yenilemeyi kıran rsync bayrağı
Aynalayan bir deploy, build'de olmayan her dosyayı siler; sertifika doğrulamasının okuduğu yol dahil. Sorunu son kullanma tarihinden haftalar önce bulun.
Deploy, build dizinini sunucuya aynalıyor. Hızlı, tekrarlanabilir ve sunucudaki durum her zaman tam olarak build'den çıkan şey. İki ay boyunca böyle çalışıyor. Sonra bir cumartesi site sertifika uyarısıyla düşüyor ve buna sebep olan deploy iki ay önce çıkmış. Zararı veren bayrak, aynalamayı aynalama yapan bayrak.
Gerçekte ne oluyor
rsync -a --delete kaynak/ host:/var/www/site/ komutu "hedefi kaynakla birebir aynı yap" demektir. Sunucuda olup build'de olmayan dosyalar silinir. Bu zaten istediğiniz davranıştır ve bu yöntemi seçme sebebinizdir: aksi hâlde adı değişen her dosya sonsuza kadar orada kalır ve dizin, kimsenin hesabını veremediği dosyalarla dolar.
Sıkıntı şu ki bir web root genelde build'den fazlasını barındırır. Orada hiçbir build'in üretmediği üç tür şey yaşar:
- Başka bir sürecin yazdığı dosyalar: kullanıcı yüklemeleri, üretilmiş sitemap'ler, önbelleğe alınmış bir dışa aktarım.
- Bir platformun ya da panelin bir kez yazdığı ve orada kalmasını beklediği dosyalar: bir rewrite config'i, bir sahiplik doğrulama dosyası.
- Sertifika otoritesinin doğrulama sırasında okuduğu, well known yolu altındaki dosyalar.
Sonuncusu zamanlaması yüzünden ilginç bir hata biçimidir. HTTP doğrulama şöyle çalışır: yenileme istemcisi bir dizine token dosyası yazar, otorite bunu düz HTTP üzerinden ister ve içerik uyuşuyorsa sertifika verilir. Yaygın bir sıkılaştırma adımı, o dizini uygulamanın tamamen dışında tutmaktır: ya web root'tan paylaşılan bir dizine symlink verilir ya da istemciye bir yol gösterilip adresi oraya eşleme işi sunucu config'ine bırakılır. Kırılan symlink'li olandır, çünkü web root içindeki bir symlink de bir dosyadır ve aynalayan deploy dosyaları siler.
Devamı, kimsenin iki olayı birbirine bağlayamayacağı kadar yavaş ilerler:
- Bir deploy symlink'i siler. Site etkilenmez, bütün sayfalar çalışır, hiçbir yere hata düşmez.
- Sunucudaki sertifikanın yetmiş günü daha vardır, yani yenileme hiç denenmez.
- Altmışıncı gün civarında timer denemeye başlar. Otorite token adresini ister ve 404 alır, ya da uygulamanın catch all sayfasını 200 durum koduyla alır.
- Deneme başarısız olur, log'a bir satır yazılır ve timer günde iki kez aynı sonucu alarak devam eder.
- On gün sonra sertifika dolar ve her ziyaretçi uyarı görür; aralarında uyarıyı geçemeyecek olanlar da vardır.
Bunun aynı biçimde görünen ama sebebi farklı bir ikinci sürümü var. Deploy bir rewrite config'ini değiştirdiyse ve uygulama router'ı artık bütün yollara sahipse, challenge adresi 200 ile ana sayfayı döner. Otorite bu kez eksik dosya değil geçersiz yanıt bildirir, siz de yönlendirme yerine dosya izinlerine bakmaya başlarsınız.
Nasıl görülür
Çalıştırılacak ilk komut deploy'un kendisidir: dry run modunda ve sadece neyi sileceğini göstererek:
rsync -avn --delete build/ deploy@host:/var/www/site/ | grep '^deleting'
# deleting .well-known/acme-challenge
# deleting uploads/2025/09/fatura-sablonu.pdf
# deleting sitemap-urunler.xmlBu üç satır, scripti bir saat okumaktan daha değerlidir. -n hiçbir değişiklik yapmaz ve deleting ön eki tam olarak "sunucuda var, build'de yok" listesidir.
Sonra kalan süreyi takvim kaydına değil sertifikanın kendisine sorun:
echo | openssl s_client -connect ornek.com:443 -servername ornek.com 2>/dev/null \
| openssl x509 -noout -enddate
# notAfter=Dec 2 08:14:31 2025 GMTArdından doğrulamanın şu anda çalışıp çalışmayacağını, kendi koyduğunuz bir dosyayla sorun:
printf 'probe\n' | sudo tee /var/lib/acme/.well-known/acme-challenge/probe >/dev/null
curl -sS -o /dev/null -w '%{http_code}\n' http://ornek.com/.well-known/acme-challenge/probe
# 404İstek düz HTTP olmalı, çünkü otorite oradan başlar. HTTPS'e yönlendirme sorun değildir ve takip edilir, ama 80 portunu kapatan bir güvenlik duvarı sorundur. probe kelimesi yerine bir sayfa HTML dönen 200 ise hatanın yönlendirme sürümüdür, o yüzden sadece durum koduna değil gövdeye de bakın.
Son olarak yenileme istemcisinin nereye yazdığını sandığına ve en son denediğinde ne olduğuna bakın:
grep -h webroot_path /etc/letsencrypt/renewal/*.conf
# webroot_path = /var/www/site,
journalctl -u certbot.service --since '60 days ago' | grep -i 'error\|failed' | tail -3Yenileme timer'ının çalışıp çalışmadığına da bakın, çünkü hiç denenmeyen bir yenileme de sonuçta aynı yere çıkar. Timer listesinde yenileme satırının bir sonraki çalışma zamanı görünmüyorsa, doğrulama yolunu düzeltseniz bile kimse onu kullanmayacak demektir.
Çözüm
Challenge'ı web root dışına taşıyın ve sunucu config'inde eşleyin, böylece hiçbir deploy ona ulaşamasın:
# uygulamanin catch all kuralini yener, cunku ^~ regex location'lardan onceliklidir
location ^~ /.well-known/acme-challenge/ {
root /var/lib/acme;
default_type text/plain;
try_files $uri =404;
}alias yerine root kullanıldığında istek yolu dizinin sonuna eklenir, yani /.well-known/acme-challenge/token adresi /var/lib/acme/.well-known/acme-challenge/token dosyasından okunur. İstemci de webroot yolu /var/lib/acme olduğunda tam olarak oraya yazar. Bir kez ayarlayın, ikisi sonsuza kadar anlaşsın:
sudo mkdir -p /var/lib/acme/.well-known/acme-challenge
sudo certbot certonly --webroot --webroot-path /var/lib/acme -d ornek.comChallenge web root içinde kalmak zorundaysa, deploy scriptinin yanında duran ve her deploy'da okunan bir filter dosyasıyla açıkça koruyun:
# deploy-filter.txt
P /.well-known/
P /uploads/
P /storage/
P /sitemap-*.xml
P /.envrsync -a --delete --filter="merge deploy-filter.txt" build/ deploy@host:/var/www/site/P kuralı bir yolu silme aşamasından korur ve aktarıma karışmaz, yani niyetinizi exclude'dan daha açık söyler. Exclude da yan etki olarak korur, ama o yan etki, biri hedefteki hariç tutulmuş dosyaları silen bayrağı eklediği anda ortadan kalkar.
Listeyi yazarken kural basittir: build'in üretmediği her yol korunur. Build'in ürettiği bir dosyayı yanlışlıkla korursanız bedeli, sunucuda kalan eski bir dosyadır ve bunu bir sonraki dağıtım düzeltir. Korunması gereken bir yolu listeye koymazsanız bedeli veri kaybıdır ve onu geri getiren bir sonraki dağıtım yoktur. İki hata aynı ağırlıkta değildir, o yüzden şüphede kaldığınızda koruyun.
Korunacakların listesini bir kere yazmaya değer, çünkü çoğu projede aynı listedir: yüklemeler, üretilen dosyalar, challenge yolu, arama konsolu ve mobil uygulama doğrulama dosyaları, bakım sayfası ve panelin yönettiği her config. Bu çözümün bedeli güncel tutulması gereken bir dosyadır. Atlamanın bedeli hafta sonu yaşanan bir sertifika kesintisidir.
Canlı dizine aynalamak yerine hazır bir dizini yerine takan bir deploy, silme aşamasını tamamen ortadan kaldırır. Pasif dizine build alıp geçiş yapmanın bir sebebi daha budur. Durumu release dışında tutma ihtiyacını ortadan kaldırmaz, sadece sınırı görünür kılar.
Çalıştığını nasıl doğrularsınız
Önce challenge'ın deploy'dan sağ çıktığını, sonra yenilemenin gerçekten çalıştığını kanıtlayın:
printf 'probe\n' | sudo tee /var/lib/acme/.well-known/acme-challenge/probe >/dev/null
curl -sS http://ornek.com/.well-known/acme-challenge/probe
# probe
./deploy.sh
curl -sS http://ornek.com/.well-known/acme-challenge/probe
# probe
sudo certbot renew --dry-run
# Congratulations, all simulated renewals succeededSıra önemlidir: probe dosyasını deploy'dan önce koyun ve sonra tekrar isteyin. Sadece deploy sonrası bakarsanız, dosyanın hiç var olmadığıyla silindiğini ayırt edemezsiniz.
Asıl test dry run'dır. Doğrulamanın tamamını bir staging ortamına karşı yapar ve gerçek bir yenileme nasıl başarısız olacaksa aynı şekilde başarısız olur; yani yönlendirme değişikliğini de, izin değişikliğini de, silinmiş bir yolu da aynı şekilde yakalar. Deploy scriptinde, sunucu config'inde veya web root düzeninde her değişiklikten sonra çalıştırın.
Sonra son kullanma mailine güvenmeyi bırakın. Yirmi bir gün kala uyaran bir kontrol size üç iş haftası kazandırır:
gun=$(( ( $(date -d "$(echo | openssl s_client -connect ornek.com:443 -servername ornek.com 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)" +%s) - $(date +%s) ) / 86400 ))
[ "$gun" -lt 21 ] && echo "sertifikanin $gun gunu kaldi"Nelere dikkat etmeli
- Hedefteki hariç tutulmuş dosyaları silen bayrak, filter dosyanızdaki bütün korumaları tek kelimeyle iptal eder. Deploy scriptinde arayın ve eskide kalmış bir dosyayı temizlemek için asla eklemeyin.
- Kaynağın sonundaki eğik çizgi neyin aynalandığını değiştirir.
build/içeriği kopyalar,builddizinin kendisini kopyalar ve ikinci biçim ilk çalıştırmada hedefteki neredeyse her şeyi siler. - Yenileme hataları istemcinin log yazdığı yere gider, ki orası genelde kimsenin açmadığı bir dosyadır. Otoritenin maili de artık okuyanı olmayan bir adrese gidiyorsa ilk bildiriminiz tarayıcı uyarısı olur. Bu, yedeğin sıfır baytlık bir dosya çıkmasıyla aynı kalıptır.
- Challenge yolundaki 404'ü, sitenin tamamında nokta ile başlayan yolların kurallarını gevşeterek çözmeyin. Well known, nokta ile başlayan yollar arasında halka açık olması gereken tek yoldur; altındaki geri kalan her şey internete kapalı kalmalıdır.
- Sunucuda duran ama kimsenin sahiplenmediği dosyalar zamanla birikir, koruma listesi de onlarla birlikte şişer. Yılda bir kez
deletinglistesini okuyup gerçekten gereken yolları ayıklayın, yoksa liste her şeyi koruyan ve hiçbir şey söylemeyen bir kurala dönüşür. - DNS doğrulaması web root'u tamamen atlar, wildcard için kullanışlıdır, ama DNS hesabınızın kimlik bilgilerini sunucuya koyar. Bu daha küçük bir risk değil, farklı bir risktir.
İşin genel hâli şu: bir deploy'un sunucuda neyin bulunması gerektiğine dair bir fikri vardır ve bu fikir her zaman gerçekten dardır. Build dışındaki bir sürecin yazdığı her şey onun için görünmezdir, dolayısıyla silinebilirdir. Hangi yolların build'i ilgilendirmediğini yazmak on dakika sürer, silme listesini gösteren dry run ise on saniye. İkisi de bir makinenin kim olduğunu kanıtlayamaz hâle geldiğini cumartesi günü öğrenmekten ucuzdur.
Sorular ve cevaplar
- Sorunsuz geçen bir deploy'dan sonra sertifika yenilemem neden bozuldu?
- Çünkü aynalayan bir deploy build'de bulunmayan dosyaları siler ve doğrulama, build'de asla bulunmayan bir dosyayı okur. Deploy'un kendisi başarılıdır, site çalışmaya devam eder, diskteki sertifika hâlâ geçerlidir, yani hiçbir şey sorun bildirmez. Bir sonraki yenileme denemesi sessizce başarısız olur ve siz durumu tarayıcı uyarı verdiğinde öğrenirsiniz.
- exclude aynı zamanda silinmekten de korur mu?
- Evet. rsync exclude listesini silme aşamasına da uygular, yani hariç tutulan bir yol ne aktarılır ne silinir. İstisnası, hedefteki hariç tutulmuş dosyaları silen bayraktır; o bayrak tam olarak bu korumayı tersine çevirir ve bir deploy scriptine kazara girmemelidir. Aynı şeyi söylemenin daha açık yolu, filter dosyasına bir protect kuralı yazmaktır.
- Challenge dizini nerede durmalı?
- Deploy'un dokunmadığı herhangi bir yerde, challenge adresini oraya eşleyen bir web sunucusu kuralıyla birlikte. Yenileme istemcisinin kendi state dizininin altındaki bir klasör iyi çalışır; sunucu config'i de well known yolunu uygulamanın catch all kuralını yenen bir prefix eşleşmesiyle oraya yönlendirir. Böylece web root her deploy'da silinip yeniden oluşturulabilir, yenileme bundan hiç etkilenmez.
- Sertifika harcamadan yenilemeyi nasıl test ederim?
- Yenileme istemcisini dry run modunda çalıştırarak. Bu mod doğrulamanın tamamını otoritenin staging ortamına karşı yapar ve diske hiçbir şey yazmaz. Gerçek bir yenileme nasıl başarısız olacaksa aynı şekilde başarısız olur, yani sözdizimi kontrolü değil gerçek bir testtir. Deploy'da, web sunucusu config'inde veya web root düzeninde her değişiklikten sonra çalıştırın.