Semptom
Bir sunucuda Caddy, ters vekil olarak bir alt alan adına hizmet veriyordu. Caddy'nin sertifikayı otomatik alması gerekiyordu; almıyordu. Log'da tek satır dönüp duruyordu:
obtaining certificate: [panel.ornek-alan.com] Obtain: ...
dial tcp 172.65.32.248:443: i/o timeout
Site ayakta görünüyordu, çünkü alan adı bir CDN'in arkasındaydı ve ziyaretçi CDN'in sertifikasını görüyordu. Yani hata kullanıcıya yansımıyordu — bu yüzden aylarca sırada bekledi. Log'da biriken deneme sayısı 136'ya çıkmıştı.
Yanlış yollar
Bu vakanın öğretici tarafı, doğru cevaptan çok kaç kere yanlış yere baktığım:
- DNS. ACME doğrulaması DNS üzerinden yapılıyordu, önce oraya baktım. Kayıtlar doğruydu, API anahtarı geçerliydi.
- Sertifika sağlayıcısı. Hız sınırına takıldığımı düşündüm. Öyle olsaydı hata mesajı hız sınırı derdi;
i/o timeoutdemezdi. - Sunucunun dış erişimi. Host'a girip elle denedim — çalıştı. Bu testin sonucu beni tam ters yöne gönderdi.
# Host üzerinden — SORUNSUZ
curl -sS -o /dev/null -w '%{http_code}\n' \
https://acme-v02.api.letsencrypt.org/directory
200
Sunucu dışarı çıkabiliyordu. Öyleyse sorun sunucuda değildi. Bu çıkarım yanlıştı ve aylarca dosyanın kapanmasını engelledi.
Doğru test
Kırılma noktası şu soruyu sormakla geldi: ben host'tan test ediyorum, ama isteği yapan host değil, container. Aynı komutu container'ın içinden çalıştırdım:
docker exec caddy wget -qO- https://acme-v02.api.letsencrypt.org/directory
# (yanıt yok — asılı kalıyor, sonra timeout)
Aynı sunucu, aynı hedef, aynı saniye. Host çıkabiliyor, container çıkamıyor. Fark buradaydı.
Kök neden
Aylar önce bu makinede bir parola kasası çalışıyordu ve 443 portunu yalnızca VPN üzerinden erişilebilir yapmak için özel bir iptables zinciri yazmıştım. Kural özet olarak şuydu:
iptables -A VAULT-FW -p tcp --dport 443 -j DROP
Niyetim açıktı: dışarıdan gelen 443 isteklerini düşür. Kuralın yaptığı ise
farklıydı. Bu zincir DOCKER-USER üzerinden çağrılıyordu ve
DOCKER-USER container trafiğini FORWARD aşamasında görür —
yani hem içeri gelen hem dışarı giden paketleri.
Container'ın sertifika sunucusuna açtığı bağlantının hedef portu da 443'tü. Kural, yönü umursamadığı için bu paketi de düşürdü. Caddy dışarı çıkamıyordu.
--dport 443 "dışarıdan gelen HTTPS" demek değildir. Yalnızca "hedef portu
443 olan paket" demektir — ve senin kendi sunucunun kurduğu her HTTPS bağlantısının
hedef portu da 443'tür. Yönü belirten şey --dport değil,
-i / -o (giriş/çıkış arayüzü) veya bağlantı durumudur.
Çözüm
Kural, yalnızca dış arayüzden gelen trafiğe uygulanacak şekilde yeniden yazıldı. Kurulu bağlantılar ve VPN ağı kuralın önünde geçiyor:
WAN_IF=eth0
VPN_NET=10.8.0.0/24
# 1) Kurulu/ilgili bağlantılar dokunulmadan geçsin
iptables -A VAULT-FW -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
# 2) VPN ağından gelen erişime izin
iptables -A VAULT-FW -s "$VPN_NET" -p tcp --dport 443 -j RETURN
# 3) DROP yalnızca dış arayüzden GELEN trafiğe
iptables -A VAULT-FW -i "$WAN_IF" -p tcp --dport 443 -j DROP
Birinci kural en kritik olanı: giden bağlantının dönüş paketleri artık zincire hiç
girmiyor. Üçüncü kuraldaki -i "$WAN_IF" ise niyeti nihayet koda çeviriyor.
certificate obtained successfully
{"level":"info","logger":"tls.obtain","msg":"releasing lock"}
Dört aydır dönen döngü, kural düzeltildikten sonraki ilk denemede kapandı.
Kalıcı ders
- Testi, isteği yapan şeyin içinden çalıştır. Host'tan yapılan test container'ın ağ yolunu kanıtlamaz. Bu vakada tek bir yanlış konumlu test dört ay kaybettirdi.
- Güvenlik kuralları sessizce yanlış çalışır. Fazla kapatan bir kural hata vermez; sadece bir şeyler "bazen olmaz". Kural yazarken yönü açıkça belirt.
- Bir servisin ölümü kullanıcıya yansımıyorsa daha tehlikelidir. CDN önde olduğu için site ayakta görünüyordu. Görünmeyen arıza, sıraya hiç girmez.
Aynı hata deseni DOCKER-USER'a kural yazan herkesi bekliyor: o zincir,
alışık olduğun INPUT zinciri gibi davranmaz.