Caddy ile TLS Mutual Authentication (mTLS) Yapılandırması

Güvenliği birkaç katmana bölmek isteyenler için mTLS, yani mutual TLS, gerçek anlamda karşılıklı kimlik doğrulama sunuyor. Klasik TLS’te sadece sunucu kendini istemciye kanıtlar; mTLS’de ise her iki taraf da sertifikasını göstermek zorunda kalır. Bu yaklaşım özellikle microservice mimarilerinde, dahili API’lerde ve sıfır güven (zero trust) ağ tasarımlarında kritik bir rol oynuyor. Caddy ise bu işi şaşırtıcı derecede az konfigürasyonla hallediyor.

mTLS Nedir ve Neden Gerekli?

Standart HTTPS bağlantısında şu olur: tarayıcın sunucuya gider, sunucu sertifikasını gösterir, tarayıcı bunu doğrular ve şifreli kanal kurulur. Sunucu sana “ben gerçekten api.example.com’um” der. Ama sen kimsin? Kimliğin sadece bir kullanıcı adı/şifre veya JWT token ile kanıtlanır. Bu yeterli değil mi? Pek çok durumda yeterli. Ama bazı senaryolarda değil.

Şöyle düşün: bir şirkette onlarca microservice birbirleriyle konuşuyor. Servis A, servis B’ye istek atıyor. Araya birisi girip servis B’ye sahte istek atabilir mi? Token çalınabilir mi? mTLS bu sorunu kökte çözüyor: servis A da kendi sertifikasını göstermek zorunda. Sertifikayı imzalayan CA (Certificate Authority) senin kontrolünde olduğunda, sadece senin imzaladığın sertifikalara sahip istemciler bağlanabiliyor.

Gerçek dünya kullanım senaryoları şöyle sıralanabilir:

  • Microservice iletişimi: Kubernetes içindeki servisler arası güvenlik katmanı
  • Dahili API güvenliği: Sadece belirli sunucuların erişebileceği yönetim API’leri
  • IoT cihaz kimlik doğrulaması: Her cihazın kendine ait sertifikası olması
  • B2B entegrasyonlar: Partner şirketlerin sisteminize erişimi
  • Zero trust ağ mimarisi: VPN olmadan güvenli erişim

Caddy’nin mTLS Desteği

Caddy, tls direktifi altında client_auth bloğu ile mTLS’i native olarak destekliyor. Harici modül gerekmez, karmaşık nginx ssl_verify_client ayarlarıyla uğraşmak gerekmez. Caddyfile sözdizimi bu işi oldukça okunabilir kılıyor.

Caddy’nin mTLS implementasyonunda şu modlar mevcut:

  • require_and_verify: İstemci sertifikası zorunlu, CA tarafından doğrulanmış olmalı
  • require: Sertifika zorunlu ama CA doğrulaması yapılmaz
  • verify_if_given: Sertifika verilmişse doğrula, verilmemişse geç
  • request: Sertifikayı iste ama zorunlu tutma

Biz ağırlıklı olarak require_and_verify modunu kullanacağız.

Ortam Hazırlığı ve CA Oluşturma

Önce kendi CA’nı oluşturman gerekiyor. Production ortamda bunun için HashiCorp Vault veya CFSSL gibi araçlar kullanabilirsin, ama öğrenme amaçlı openssl ile hızlıca halledebiliriz.

# CA için dizin yapısı oluştur
mkdir -p /etc/caddy/certs/{ca,server,clients}
cd /etc/caddy/certs

# CA private key oluştur
openssl genrsa -out ca/ca.key 4096

# CA self-signed sertifika oluştur (10 yıl geçerli)
openssl req -new -x509 -days 3650 -key ca/ca.key 
  -out ca/ca.crt 
  -subj "/C=TR/ST=Istanbul/O=MyCompany/OU=Infrastructure/CN=MyCompany-Internal-CA"

# CA sertifikasını doğrula
openssl x509 -in ca/ca.crt -text -noout | grep -E "Subject:|Issuer:|Not"

Şimdi sunucu sertifikası oluşturalım. Caddy’nin otomatik TLS’ini kullanıyorsan buna gerek yok, ama dahili bir ortamda çoğu zaman kendi sertifikalarını yönetmen gerekir:

# Sunucu private key
openssl genrsa -out server/server.key 2048

# Sunucu CSR (Certificate Signing Request)
openssl req -new -key server/server.key 
  -out server/server.csr 
  -subj "/C=TR/ST=Istanbul/O=MyCompany/CN=api.internal.example.com"

# SAN (Subject Alternative Names) ile sunucu sertifikasını imzala
cat > /tmp/server_ext.cnf << EOF
[req]
req_extensions = v3_req
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = api.internal.example.com
DNS.2 = *.internal.example.com
IP.1 = 10.0.0.10
EOF

openssl x509 -req -days 365 -in server/server.csr 
  -CA ca/ca.crt -CAkey ca/ca.key 
  -CAcreateserial 
  -out server/server.crt 
  -extfile /tmp/server_ext.cnf 
  -extensions v3_req

İstemci sertifikaları oluşturmak için bir script hazırlamak işleri kolaylaştırır. Özellikle birden fazla servise veya kullanıcıya sertifika dağıtacaksan:

#!/bin/bash
# create_client_cert.sh
# Kullanım: ./create_client_cert.sh service-name

CLIENT_NAME=$1
CERTS_DIR="/etc/caddy/certs"

if [ -z "$CLIENT_NAME" ]; then
  echo "Kullanim: $0 <client-name>"
  exit 1
fi

mkdir -p "${CERTS_DIR}/clients/${CLIENT_NAME}"

# Client private key
openssl genrsa -out "${CERTS_DIR}/clients/${CLIENT_NAME}/${CLIENT_NAME}.key" 2048

# Client CSR
openssl req -new 
  -key "${CERTS_DIR}/clients/${CLIENT_NAME}/${CLIENT_NAME}.key" 
  -out "${CERTS_DIR}/clients/${CLIENT_NAME}/${CLIENT_NAME}.csr" 
  -subj "/C=TR/ST=Istanbul/O=MyCompany/OU=Services/CN=${CLIENT_NAME}"

# Client sertifikasini CA ile imzala
openssl x509 -req -days 365 
  -in "${CERTS_DIR}/clients/${CLIENT_NAME}/${CLIENT_NAME}.csr" 
  -CA "${CERTS_DIR}/ca/ca.crt" 
  -CAkey "${CERTS_DIR}/ca/ca.key" 
  -CAcreateserial 
  -out "${CERTS_DIR}/clients/${CLIENT_NAME}/${CLIENT_NAME}.crt"

echo "Sertifika olusturuldu: ${CERTS_DIR}/clients/${CLIENT_NAME}/"
echo "Key: ${CLIENT_NAME}.key"
echo "Cert: ${CLIENT_NAME}.crt"
echo "CA: ${CERTS_DIR}/ca/ca.crt"

# PFX/P12 formatinda da olustur (bazi uygulamalar icin)
openssl pkcs12 -export 
  -out "${CERTS_DIR}/clients/${CLIENT_NAME}/${CLIENT_NAME}.p12" 
  -inkey "${CERTS_DIR}/clients/${CLIENT_NAME}/${CLIENT_NAME}.key" 
  -in "${CERTS_DIR}/clients/${CLIENT_NAME}/${CLIENT_NAME}.crt" 
  -certfile "${CERTS_DIR}/ca/ca.crt" 
  -passout pass:""

Bu scripti çalıştırarak birkaç istemci sertifikası oluştur:

chmod +x create_client_cert.sh
./create_client_cert.sh order-service
./create_client_cert.sh payment-service
./create_client_cert.sh monitoring-agent

Caddy Yapılandırması

Basit mTLS Konfigürasyonu

En temel haliyle bir Caddyfile:

api.internal.example.com {
    tls /etc/caddy/certs/server/server.crt /etc/caddy/certs/server/server.key {
        client_auth {
            mode require_and_verify
            trusted_ca_cert_file /etc/caddy/certs/ca/ca.crt
        }
    }

    reverse_proxy localhost:8080
}

Bu kadar. Ciddi söylüyorum; nginx’te aynı şeyi yapmak için ssl_client_certificate, ssl_verify_client, ssl_verify_depth gibi birden fazla direktif gerekir ve sonra hata ayıklamak için saatler harcarsın.

Gelişmiş Senaryo: Farklı Endpoint’lere Farklı Erişim

Microservice ortamında bazı endpoint’ler herkese açık, bazıları sadece dahili servislere özel olabilir. Caddy’de bunu route blokları ile yönetirsin:

api.example.com {
    # Public endpoint - mTLS yok
    route /public/* {
        reverse_proxy localhost:8080
    }

    # Dahili servisler icin mTLS zorunlu
    route /internal/* {
        tls {
            client_auth {
                mode require_and_verify
                trusted_ca_cert_file /etc/caddy/certs/ca/ca.crt
            }
        }
        reverse_proxy localhost:8081
    }

    # Admin endpoint - daha kısıtlı
    route /admin/* {
        tls {
            client_auth {
                mode require_and_verify
                trusted_ca_cert_file /etc/caddy/certs/ca/ca.crt
            }
        }
        # IP kontrolü de ekle
        @not_internal {
            not remote_ip 10.0.0.0/8
        }
        respond @not_internal "Forbidden" 403

        reverse_proxy localhost:8082
    }
}

Dikkat: tls direktifi aslında site bloğu seviyesinde çalışır, route seviyesinde client_auth yapamazsın doğrudan. Yukarıdaki yapı konsepti göstermek içindir; gerçekte ayrı site blokları veya farklı portlar kullanman daha sağlıklı olur.

JSON API ile Gelişmiş Konfigürasyon

Caddy’nin gerçek gücü JSON API’sindedir. Caddyfile’ın sınırlarına takıldığında JSON konfigürasyonuna geçmek mantıklı:

{
  "apps": {
    "http": {
      "servers": {
        "secure_api": {
          "listen": [":443"],
          "routes": [
            {
              "match": [{"host": ["api.internal.example.com"]}],
              "handle": [
                {
                  "handler": "reverse_proxy",
                  "upstreams": [{"dial": "localhost:8080"}],
                  "headers": {
                    "request": {
                      "set": {
                        "X-Client-Cert-CN": ["{http.request.tls.client.subject.cn}"],
                        "X-Client-Cert-Serial": ["{http.request.tls.client.serial}"]
                      }
                    }
                  }
                }
              ]
            }
          ],
          "tls_connection_policies": [
            {
              "match": {"sni": ["api.internal.example.com"]},
              "certificate_selection": {
                "any_tag": ["internal"]
              },
              "client_authentication": {
                "trusted_ca_certs_pem_files": ["/etc/caddy/certs/ca/ca.crt"],
                "mode": "require_and_verify"
              }
            }
          ]
        }
      }
    }
  }
}

Bu konfigürasyonu curl ile Caddy Admin API’ye yükleyebilirsin:

curl -X POST "http://localhost:2019/load" 
  -H "Content-Type: application/json" 
  -d @/etc/caddy/config.json

İstemci Bilgilerini Backend’e İletmek

mTLS’in güzel yanlarından biri, istemcinin kim olduğunu backend uygulamana header olarak iletebilmek. Caddy bu bilgileri placeholder’lar aracılığıyla sunuyor:

api.internal.example.com {
    tls /etc/caddy/certs/server/server.crt /etc/caddy/certs/server/server.key {
        client_auth {
            mode require_and_verify
            trusted_ca_cert_file /etc/caddy/certs/ca/ca.crt
        }
    }

    # İstemci sertifika bilgilerini header olarak ilet
    header_up X-Client-CN {tls_client_subject_cn}
    header_up X-Client-Issuer {tls_client_issuer_cn}
    header_up X-Client-Serial {tls_client_serial}
    header_up X-Client-Fingerprint {tls_client_fingerprint}

    reverse_proxy localhost:8080
}

Backend uygulamanın bu header’ları okuyarak kimin istek attığını anlaması çok kolay. Örneğin Python Flask uygulamasında:

from flask import Flask, request

app = Flask(__name__)

@app.route('/api/data')
def get_data():
    client_cn = request.headers.get('X-Client-CN', 'unknown')
    client_serial = request.headers.get('X-Client-Serial', 'unknown')
    
    app.logger.info(f"Request from: {client_cn} (serial: {client_serial})")
    
    # CN'e göre yetkilendirme mantığı
    allowed_services = ['order-service', 'payment-service']
    if client_cn not in allowed_services:
        return {'error': 'Unauthorized service'}, 403
    
    return {'data': 'secret stuff', 'client': client_cn}

Test ve Doğrulama

Kurulumu test etmek için curl kullanılır. Önce sertifikasız bağlanmayı dene:

# Sertifikasız deneme - bağlantı reddedilmeli
curl -v --cacert /etc/caddy/certs/ca/ca.crt 
  https://api.internal.example.com/api/data

# Hata: "alert handshake failure" veya "SSL alert number 40" görmelisin

Şimdi doğru istemci sertifikasıyla bağlan:

# Gecerli istemci sertifikasıyla
curl -v 
  --cacert /etc/caddy/certs/ca/ca.crt 
  --cert /etc/caddy/certs/clients/order-service/order-service.crt 
  --key /etc/caddy/certs/clients/order-service/order-service.key 
  https://api.internal.example.com/api/data

# Başarılı yanıt beklenir

Sertifika bilgilerini detaylı görmek için:

# TLS handshake detayları
openssl s_client 
  -connect api.internal.example.com:443 
  -cert /etc/caddy/certs/clients/order-service/order-service.crt 
  -key /etc/caddy/certs/clients/order-service/order-service.key 
  -CAfile /etc/caddy/certs/ca/ca.crt 
  -showcerts 
  -status

# Sadece handshake ozeti
echo | openssl s_client 
  -connect api.internal.example.com:443 
  -cert /etc/caddy/certs/clients/order-service/order-service.crt 
  -key /etc/caddy/certs/clients/order-service/order-service.key 
  -CAfile /etc/caddy/certs/ca/ca.crt 2>&1 | 
  grep -E "Verify|CN=|subject|issuer"

Sertifika İptal (Revocation) Yönetimi

Bir sertifikayı geçersiz kılman gerektiğinde ne yaparsın? Örneğin bir servis tehlikeye girdi veya artık kullanılmıyor. Bunun için CRL (Certificate Revocation List) kullanırsın:

# CRL database hazırlığı
mkdir -p /etc/caddy/certs/ca/crl
touch /etc/caddy/certs/ca/crl/index.txt
echo "01" > /etc/caddy/certs/ca/crl/crlnumber

# OpenSSL CA config dosyası
cat > /etc/caddy/certs/ca/ca.conf << EOF
[ca]
default_ca = CA_default

[CA_default]
dir = /etc/caddy/certs/ca
certs = $dir/clients
new_certs_dir = $dir/clients
database = $dir/crl/index.txt
serial = $dir/crl/crlnumber
crl = $dir/crl/ca.crl
private_key = $dir/ca.key
certificate = $dir/ca.crt
default_md = sha256
policy = policy_loose

[policy_loose]
countryName = optional
stateOrProvinceName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

[crl_ext]
authorityKeyIdentifier = keyid:always
EOF

# Bir sertifikayı iptal et
openssl ca -config /etc/caddy/certs/ca/ca.conf 
  -revoke /etc/caddy/certs/clients/compromised-service/compromised-service.crt

# CRL dosyasını güncelle
openssl ca -config /etc/caddy/certs/ca/ca.conf 
  -gencrl -out /etc/caddy/certs/ca/crl/ca.crl

# CRL içeriğini doğrula
openssl crl -in /etc/caddy/certs/ca/crl/ca.crl -text -noout

Caddy CRL dosyasını şu şekilde kullanır:

api.internal.example.com {
    tls /etc/caddy/certs/server/server.crt /etc/caddy/certs/server/server.key {
        client_auth {
            mode require_and_verify
            trusted_ca_cert_file /etc/caddy/certs/ca/ca.crt
        }
    }
    reverse_proxy localhost:8080
}

Caddy doğrudan CRL dosyası direktifini Caddyfile sözdiziminde desteklemez; bunun için JSON API konfigürasyonunu veya OCSP (Online Certificate Status Protocol) kullanman gerekir. Alternatif olarak iptal edilen sertifikaların CN değerlerini uygulama katmanında filtreleyen bir mekanizma kurabilirsin.

Sertifika Rotasyonu ve Otomasyon

Üretim ortamında sertifikaların süresinin dolmasını beklemeden düzenli rotasyon yapman gerekir. Şöyle bir cronjob ile bunu otomatikleştirebilirsin:

#!/bin/bash
# rotate_client_certs.sh - 30 gun kala yenile

CERTS_DIR="/etc/caddy/certs"
DAYS_BEFORE_EXPIRY=30
ALERT_EMAIL="[email protected]"

for client_dir in "${CERTS_DIR}/clients"/*/; do
    client_name=$(basename "$client_dir")
    cert_file="${client_dir}/${client_name}.crt"
    
    if [ ! -f "$cert_file" ]; then
        continue
    fi
    
    # Son kullanma tarihini kontrol et
    expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" | cut -d= -f2)
    expiry_epoch=$(date -d "$expiry_date" +%s)
    current_epoch=$(date +%s)
    days_remaining=$(( (expiry_epoch - current_epoch) / 86400 ))
    
    echo "[$client_name] Son kullanma: $expiry_date ($days_remaining gun kaldi)"
    
    if [ "$days_remaining" -lt "$DAYS_BEFORE_EXPIRY" ]; then
        echo "[$client_name] YENILENIYOR..."
        
        # Yeni sertifika olustur
        openssl genrsa -out "${client_dir}/${client_name}.key.new" 2048
        
        openssl req -new 
          -key "${client_dir}/${client_name}.key.new" 
          -out "${client_dir}/${client_name}.csr" 
          -subj "/C=TR/ST=Istanbul/O=MyCompany/OU=Services/CN=${client_name}"
        
        openssl x509 -req -days 365 
          -in "${client_dir}/${client_name}.csr" 
          -CA "${CERTS_DIR}/ca/ca.crt" 
          -CAkey "${CERTS_DIR}/ca/ca.key" 
          -CAcreateserial 
          -out "${client_dir}/${client_name}.crt.new"
        
        # Eski sertifikayı yedekle, yenisini etkinleştir
        cp "${client_dir}/${client_name}.crt" "${client_dir}/${client_name}.crt.bak"
        cp "${client_dir}/${client_name}.key" "${client_dir}/${client_name}.key.bak"
        mv "${client_dir}/${client_name}.crt.new" "${client_dir}/${client_name}.crt"
        mv "${client_dir}/${client_name}.key.new" "${client_dir}/${client_name}.key"
        
        # Bildiri gonder
        echo "[$client_name] sertifikasi yenilendi." | 
          mail -s "Cert Rotated: $client_name" "$ALERT_EMAIL"
    fi
done

Bu scripti crontab’a ekle:

# Her gün sabah 7'de çalıştır
0 7 * * * /opt/scripts/rotate_client_certs.sh >> /var/log/cert-rotation.log 2>&1

Yaygın Sorunlar ve Çözümleri

Handshake hatası alıyorum ama sertifika doğru görünüyor:

Çoğunlukla sertifika zinciri eksiktir. CA sertifikasının tam zinciri içerip içermediğini kontrol et. Ara CA varsa bundle dosyası oluşturman gerekir:

# Ara CA varsa bundle olustur
cat /etc/caddy/certs/ca/intermediate.crt 
    /etc/caddy/certs/ca/root-ca.crt > /etc/caddy/certs/ca/ca-bundle.crt

Caddy loglarında “tls: certificate required” hatası:

Bu aslında beklenen davranış; istemci sertifika göndermemiş demek. Loglama seviyesini artırarak daha fazla detay alabilirsin:

# Caddy'i debug modda başlat
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile 2>&1 | 
  grep -E "tls|cert|handshake"

İzin hataları:

Caddy genellikle caddy kullanıcısı ile çalışır. Sertifika dosyaları bu kullanıcı tarafından okunabilir olmalı:

chown -R caddy:caddy /etc/caddy/certs
chmod 600 /etc/caddy/certs/ca/ca.key
chmod 644 /etc/caddy/certs/ca/ca.crt
chmod 600 /etc/caddy/certs/server/server.key
chmod 644 /etc/caddy/certs/server/server.crt

Sonuç

Caddy ile mTLS kurulumu, nginx veya HAProxy’ye kıyasla gerçekten daha az zahmetli. Temel konsept basit: kendi CA’nı oluştur, sunucu ve istemci sertifikalarını imzala, Caddyfile’a client_auth bloğunu ekle. Ama üretim ortamında işin asıl zorluğu teknik konfigürasyondan çok operasyonel kısımda: sertifika rotasyonu, iptal mekanizmaları, yeni servislere sertifika dağıtımı ve bunların dokümantasyonu.

Eğer microservice mimarisi kuruyorsan mTLS’i en baştan tasarıma dahil etmeni öneririm. Sonradan eklemek hem mimari hem de operasyonel açıdan daha zahmetli oluyor. Caddy’nin bu işi bu kadar kolay hale getirmesi, küçük ve orta ölçekli ekipler için özellikle değerli; ayrı bir service mesh çözümü (Istio gibi) kurmaya gerek kalmadan makul düzeyde zero trust güvenlik mimarisi kurabiliyorsun. Büyük ölçekte iseniz tabii ki Vault gibi bir secrets yönetim sistemi ve sertifika otomasyonu için cert-manager gibi araçlara bakmak mantıklı.

Yorum yapın