Caddy ile On-Demand TLS Kullanarak Dinamik Sertifika Yönetimi

Yüzlerce, hatta binlerce subdomain’i olan bir platform işlettiğinizi düşünün. Her müşteri kendi özel alan adını sisteminize bağlamak istiyor ve siz her biri için manuel olarak sertifika almanız gerekiyor. Geleneksel yaklaşımla bu iş kabusu gibi görünür. İşte tam burada Caddy’nin On-Demand TLS özelliği devreye giriyor ve hayatınızı köklü biçimde değiştiriyor.

Caddy, modern web sunucuları arasında TLS yönetimini en zarif şekilde çözen araç olma unvanını hak ediyor. Standart ACME protokolü ile zaten otomatik sertifika alıyor, ama On-Demand TLS bunu bir adım öteye taşıyor: İlk HTTPS bağlantısı geldiği anda, o alan adı için gerçek zamanlı sertifika istiyor ve sunuyor. Hiç yapılandırma satırı yazmadan, hiç müdahale etmeden.

On-Demand TLS Nedir ve Neden Gerekir?

Klasik sertifika yönetiminde bir sorun vardır: Sertifikayı almak için alan adını önceden bilmeniz gerekir. Wildcard sertifikalar kısmen çözüm sunar, ama DNS-01 challenge gerektirir ve tüm subdomainleri kapsamaz. Üstelik farklı müşterilerin kendi alan adlarını sisteminize yönlendirdiği senaryolarda wildcard işe yaramaz.

On-Demand TLS şu şekilde çalışır:

  • Caddy, henüz sertifikası olmayan bir alan adına HTTPS isteği geldiğinde onu yakalar
  • Arka planda ACME sunucusuna (genellikle Let’s Encrypt) sertifika talebi gönderir
  • Sertifika gelene kadar bağlantıyı bekletir veya yeniden dener
  • Sertifika önbelleğe alınır ve sonraki isteklerde anında sunulur
  • Yenileme de tamamen otomatik gerçekleşir

Bu mekanizma özellikle şu senaryolarda hayat kurtarır:

  • SaaS platformları: Her müşteri musteri.sizinplatform.com gibi özel subdomain kullanıyor
  • White-label ürünler: Müşteriler kendi alan adlarını (musteri.com) sisteminize CNAME ile yönlendiriyor
  • Çok kiracılı mimariler: Onlarca uygulamayı tek Caddy instance’ı üzerinden sunuyorsunuz
  • Dinamik site oluşturucular: Kullanıcılar yeni alan adları tanımlayabiliyor

Caddy Kurulumu

Önce Caddy’nin güncel sürümünü sisteminize kuralım. Ubuntu/Debian için:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | 
  sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | 
  sudo tee /etc/apt/sources.list.d/caddy-stable.list

sudo apt update
sudo apt install caddy

RHEL/CentOS tabanlı sistemler için:

dnf install 'dnf-command(copr)'
dnf copr enable @caddy/caddy
dnf install caddy

Kurulum sonrası servisi etkinleştirin:

sudo systemctl enable --now caddy
sudo systemctl status caddy

Caddy’nin versiyonunu ve mevcut modülleri görmek için:

caddy version
caddy list-modules | grep tls

Temel On-Demand TLS Yapılandırması

Caddy’de On-Demand TLS yapılandırmanın en basit yolu Caddyfile kullanmaktır. Ama dikkat: On-Demand TLS’i açık bırakmak güvenlik riski taşır. Herhangi biri sunucunuza istediği alan adı için sertifika talep ettirebilir ve bu Let’s Encrypt rate limit’lerinizi bitirir.

Bu yüzden mutlaka bir ask endpoint’i veya izin listesi tanımlamanız gerekir.

İşte temel bir Caddyfile örneği:

{
    on_demand_tls {
        ask http://localhost:9090/check
        interval 2m
        burst 5
    }
}

:443 {
    tls {
        on_demand
    }
    reverse_proxy localhost:8080
}

Buradaki parametreler:

  • ask: Yeni bir alan adı için sertifika talep gelmeden önce bu URL’e GET isteği atılır. HTTP 200 dönerse sertifika alınır, başka bir kod dönerse reddedilir
  • interval: Belirtilen süre içinde en fazla burst kadar yeni sertifika alınabilir
  • burst: İzin verilen maksimum eş zamanlı yeni sertifika sayısı

Ask Endpoint Yazmak

Ask endpoint’i basit bir HTTP servisi olabilir. Python ile hızlıca yazalım:

#!/usr/bin/env python3
# /opt/caddy-ask/server.py

from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import json

# İzin verilen alan adları veritabanı
# Gerçek uygulamada bunu veritabanından çekersiniz
ALLOWED_DOMAINS = {
    "musteri1.example.com",
    "musteri2.example.com", 
    "ozelalan.com",
    "baska-musteri.net"
}

class AskHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urlparse(self.path)
        params = parse_qs(parsed.query)
        
        domain = params.get('domain', [None])[0]
        
        if domain and domain in ALLOWED_DOMAINS:
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b'OK')
        else:
            self.send_response(403)
            self.end_headers()
            self.wfile.write(b'Forbidden')
    
    def log_message(self, format, *args):
        # İsteğe bağlı: loglama
        print(f"Ask check: {args}")

if __name__ == '__main__':
    server = HTTPServer(('localhost', 9090), AskHandler)
    print("Ask server 9090 portunda dinleniyor...")
    server.serve_forever()

Bu servisi systemd ile ayağa kaldırın:

sudo tee /etc/systemd/system/caddy-ask.service << 'EOF'
[Unit]
Description=Caddy On-Demand TLS Ask Service
After=network.target

[Service]
Type=simple
User=caddy
ExecStart=/usr/bin/python3 /opt/caddy-ask/server.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl enable --now caddy-ask

Gerçek Dünya Senaryosu: SaaS Platform

Bir proje yönetim SaaS’ı işlettiğinizi varsayalım. Her müşteri {musteri-slug}.projeyonetim.com adresini kullanıyor ve premium müşteriler kendi alan adlarını (projeler.onlarinsirketi.com) sisteme bağlayabiliyor.

Veritabanı destekli bir ask servisi için Go ile daha sağlam bir örnek:

// /opt/caddy-ask/main.go
package main

import (
    "database/sql"
    "log"
    "net/http"
    "os"
    
    _ "github.com/lib/pq"
)

var db *sql.DB

func checkDomain(w http.ResponseWriter, r *http.Request) {
    domain := r.URL.Query().Get("domain")
    if domain == "" {
        http.Error(w, "domain parametresi eksik", http.StatusBadRequest)
        return
    }
    
    var exists bool
    err := db.QueryRow(`
        SELECT EXISTS(
            SELECT 1 FROM tenant_domains 
            WHERE domain = $1 
            AND is_active = true 
            AND ssl_enabled = true
        )
    `, domain).Scan(&exists)
    
    if err != nil {
        log.Printf("DB hatasi: %v", err)
        http.Error(w, "Sunucu hatasi", http.StatusInternalServerError)
        return
    }
    
    if exists {
        log.Printf("Sertifika izni verildi: %s", domain)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    } else {
        log.Printf("Sertifika reddedildi: %s", domain)
        http.Error(w, "Alan adi kayitli degil", http.StatusForbidden)
    }
}

func main() {
    dsn := os.Getenv("DATABASE_URL")
    var err error
    db, err = sql.Open("postgres", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    
    http.HandleFunc("/check", checkDomain)
    log.Println("Ask servisi :9090 portunda baslatildi")
    log.Fatal(http.ListenAndServe(":9090", nil))
}

Bu senaryoda Caddyfile yapılandırması şöyle olur:

{
    email [email protected]
    
    on_demand_tls {
        ask http://localhost:9090/check
        interval 1m
        burst 10
    }
}

# Wildcard subdomain için
*.projeyonetim.com {
    tls {
        on_demand
    }
    
    @api host api.projeyonetim.com
    handle @api {
        reverse_proxy localhost:3001
    }
    
    @app {
        host_regexp tenant ^(.+).projeyonetim.com$
    }
    handle @app {
        reverse_proxy localhost:8080 {
            header_up X-Tenant-Domain {host}
        }
    }
}

# Müşteri özel alan adları için
:443 {
    tls {
        on_demand
    }
    
    reverse_proxy localhost:8080 {
        header_up X-Custom-Domain {host}
    }
}

JSON API ile Gelişmiş Yapılandırma

Caddyfile yerine Caddy’nin JSON API’sini kullanarak runtime’da yapılandırma değiştirebilirsiniz. Bu özellikle orkestrasyon araçlarıyla entegrasyon için değerlidir:

# Mevcut yapılandırmayı görüntüle
curl -s http://localhost:2019/config/ | python3 -m json.tool

# On-demand TLS ile yeni bir route ekle
curl -X POST http://localhost:2019/config/apps/http/servers/srv0/routes 
  -H "Content-Type: application/json" 
  -d '{
    "match": [{"host": ["yeni-musteri.com"]}],
    "handle": [
      {
        "handler": "subroute",
        "routes": [{
          "handle": [{
            "handler": "reverse_proxy",
            "upstreams": [{"dial": "localhost:8080"}]
          }]
        }]
      }
    ],
    "terminal": true
  }'

Mevcut sertifikaların durumunu kontrol etmek için:

# Yüklü sertifikaları listele
curl -s http://localhost:2019/pki/ca/local | python3 -m json.tool

# Caddy'nin yönettiği sertifika bilgileri
caddy environ | grep -i cert

Sertifika Önbellekleme ve Depolama

Varsayılan olarak Caddy sertifikaları şu konumda depolar:

  • Linux: /var/lib/caddy/.local/share/caddy/
  • Root olarak çalışıyorsa: /root/.local/share/caddy/

Birden fazla Caddy instance’ı çalıştırıyorsanız (yük dengeleme senaryosu), sertifika depolama alanını merkezi hale getirmeniz gerekir. Redis veya S3 ile bunu yapabilirsiniz:

{
    storage redis {
        host     localhost
        port     6379
        password "guclu-sifre"
        db       0
        key_prefix "caddy"
        timeout  5
    }
    
    on_demand_tls {
        ask http://localhost:9090/check
        interval 2m
        burst 5
    }
}

Redis storage modülü için Caddy’yi özel modülle derlemeniz gerekir:

# xcaddy aracını kur
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Redis modülü ile derle
xcaddy build 
    --with github.com/gamalan/caddy-tlsredis

# Derlenen binary'yi yerleştir
sudo mv caddy /usr/bin/caddy
sudo setcap cap_net_bind_service=+ep /usr/bin/caddy
sudo systemctl restart caddy

Rate Limiting ve Güvenlik Önlemleri

On-Demand TLS’in en büyük riski kötüye kullanım. Birisi sunucunuzu hedef alarak binlerce farklı alan adı için sertifika talep etmeye çalışabilir. Bunu önlemek için katmanlı güvenlik önlemleri alın.

Fail2ban ile Caddy loglarını izleyin:

# /etc/fail2ban/filter.d/caddy-tls.conf
sudo tee /etc/fail2ban/filter.d/caddy-tls.conf << 'EOF'
[Definition]
failregex = .*obtaining certificate.*<HOST>.*failed
            .*tls: no certificate.*<HOST>
ignoreregex =
EOF

# /etc/fail2ban/jail.d/caddy-tls.conf
sudo tee /etc/fail2ban/jail.d/caddy-tls.conf << 'EOF'
[caddy-tls]
enabled  = true
port     = 443
filter   = caddy-tls
logpath  = /var/log/caddy/access.log
maxretry = 5
findtime = 300
bantime  = 3600
EOF

sudo systemctl restart fail2ban

Caddy’nin kendi rate limiting modülünü de ekleyin:

{
    on_demand_tls {
        ask http://localhost:9090/check
        interval 2m
        burst 3
    }
}

:443 {
    tls {
        on_demand
    }
    
    rate_limit {
        zone tls_zone {
            key {remote_host}
            window 1m
            events 10
        }
    }
    
    reverse_proxy localhost:8080
}

Loglama ve İzleme

On-Demand TLS süreçlerini takip etmek için Caddy’nin yapılandırılmış loglamasını kullanın:

{
    log {
        output file /var/log/caddy/access.log {
            roll_size 100mb
            roll_keep 10
        }
        format json
        level INFO
    }
    
    on_demand_tls {
        ask http://localhost:9090/check
        interval 2m
        burst 5
    }
}

Logları analiz etmek için:

# TLS sertifika olaylarını filtrele
sudo journalctl -u caddy -f | grep -i "tls|certificate|acme"

# JSON loglarından sertifika sorunlarını çek
sudo cat /var/log/caddy/access.log | 
  python3 -c "
import sys, json
for line in sys.stdin:
    try:
        log = json.loads(line)
        if 'tls' in str(log).lower() or 'certificate' in str(log).lower():
            print(json.dumps(log, indent=2))
    except:
        pass
" | head -100

# Sertifika yenileme durumu
sudo journalctl -u caddy --since "24 hours ago" | grep "certificate"

Staging Ortamında Test

Production’a geçmeden önce Let’s Encrypt staging ortamında test edin. Bu sayede rate limit sorunları yaşamazsınız:

{
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
    email [email protected]
    
    on_demand_tls {
        ask http://localhost:9090/check
        interval 1m
        burst 20
    }
}

:443 {
    tls {
        on_demand
    }
    reverse_proxy localhost:8080
}

Test sonrası acme_ca satırını kaldırdığınızda otomatik olarak production Let’s Encrypt kullanılır.

Sorun Giderme

On-Demand TLS’de en sık karşılaşılan sorunlar ve çözümleri:

Sertifika alınamıyor, TLS handshake hatası:

# Caddy'nin 80 portuna erişebildiğinden emin olun (HTTP-01 challenge için)
sudo ss -tlnp | grep :80
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# DNS kaydını doğrulayın
dig +short musteri.com
curl -I http://musteri.com

# Caddy'nin ask endpoint'ine ulaşabildiğini test edin
curl -v "http://localhost:9090/check?domain=musteri.com"

Let’s Encrypt rate limit aşıldı:

# Mevcut sertifika sayısını kontrol edin
ls /var/lib/caddy/.local/share/caddy/certificates/ | wc -l

# Rate limit durumunu Let's Encrypt'ten sorgula
curl -s "https://crt.sh/?q=%.projeyonetim.com&output=json" | 
  python3 -c "import sys,json; data=json.load(sys.stdin); print(len(data), 'sertifika bulundu')"

Ask endpoint yanıt vermiyor:

# Servis durumu
sudo systemctl status caddy-ask

# Port dinleniyor mu?
sudo ss -tlnp | grep 9090

# Manuel test
curl -v "http://localhost:9090/check?domain=test.com"

Sonuç

Caddy’nin On-Demand TLS özelliği, dinamik alan adı yönetimini gerçekten zahmetsiz hale getiriyor. Yüzlerce müşterinin özel alan adını yönetmek için artık Ansible playbook’ları veya certbot cronjob’ları yazmak zorunda değilsiniz.

Ancak bu güçlü özelliği kullanırken birkaç noktayı asla atlamayın: ask endpoint olmadan On-Demand TLS kullanmayın, rate limiting parametrelerini gerçekçi değerlere ayarlayın ve her zaman önce staging ortamında test edin. Let’s Encrypt’in haftalık 50 sertifika limiti ve saatlik 5 başarısız istek limiti var; bu limitleri aşmak production sisteminizi saatlerce TLS’siz bırakabilir.

Eğer çok node’lu bir yapı kuruyorsanız Redis tabanlı merkezi sertifika depolaması şart. Aksi takdirde her node aynı domain için ayrı sertifika almaya çalışır ve rate limit’leri kısa sürede eritirsiniz.

Son olarak, izleme konusunu hafife almayın. Caddy’nin JSON logları ve journalctl çıktıları sertifika yaşam döngüsünü takip etmek için oldukça yeterli. Bir Grafana dashboard’u kurarak sertifika yenileme başarı oranlarını, ask endpoint yanıt sürelerini ve başarısız TLS handshake sayılarını görselleştirirseniz, sorunları kullanıcılar fark etmeden önce yakalayabilirsiniz.

Yorum yapın