PEM'den Çalışan Stack'e — Bölüm 4: Bir Servisin İki Dünyaya Güvenmesi Gerektiğinde
Serinin finali. Sertifikalar temizdi, parolalar tekleştirilmişti, yollar doğruydu. Bir servis hâlâ başlamayı reddediyordu — ve bu sefer neden dosyalardaki bir hata değil, güven mimarisine dair temel bir yanlış anlamaydı.
Geri adım gibi görünen bir ileri adım
Ara katman nihayet dahili auth sunucusuna güvenmeye başladıktan sonra yeni bir güven hatası belirdi. İlk bakışta sinir bozucu — yine PKIX. Ama hatadaki issuer değişmişti:
Before: https://keycloak.example.local:5443/... <- the internal auth server Now: https://auth-provider.example.com/... <- a public cloud service
Bu aslında bir başarıydı: dahili sunucu artık güvensiz olarak işaretlenmiyordu. Hata şimdi ikinci bir kimlik doğrulama sağlayıcısını kapsıyordu — ücretli bir eklenti modülü için gereken public bir bulut servisi.
Ders: inatçı bir hatada bir ayrıntı değiştiğinde (burada: hata mesajındaki ana makine adı), bu önceki engelin aşıldığının işaretidir. Hata mesajlarını dikkatle okuyun — farklar bilginin ta kendisidir.
Asıl sorun: çok dar bir truststore
Konfigürasyon, servisi bir Java seçeneği aracılığıyla kendi oluşturduğum truststore'a bağlıyordu:
-Djavax.net.ssl.trustStore=/mw/root.p12
Burada Java'nın bilmeniz gereken bir özelliği devreye giriyor: kendi truststore'unuzu belirlediğiniz anda, varsayılan truststore'u tamamen devre dışı bırakmış olursunuz. Servis artık yalnızca dahili CA'ma güveniyordu — ve tek bir public CA'ya bile güvenmiyordu artık.
Auth sunucusuna yapılan dahili bağlantı için bu mükemmeldi. Ama public bulut sağlayıcısı, her tarayıcının tanıdığı türden gayet sıradan bir public CA'nın sertifikasını kullanıyordu. O artık eksikti — güven koptu.
Çözüm: tek bir truststore'da iki dünya
Gereksinim netti: servis hem dahili CA'ma hem de public CA'lara aynı anda güvenmek zorunda. Temiz çözüm, Java'nın yerleşik varsayılan truststore'unu (cacerts — tüm public CA'ları zaten içeriyor) temel olarak alıp kendi CA'mı eklemek.
Varsayılan cacerts, JDK içinde bulunur ve genellikle changeit parolasına sahiptir. Ama servisimiz farklı bir değer beklediğinden, kopyalarken store parolasını değiştirmemiz ve ardından kendi CA'mızı içe aktarmamız gerekiyor — en iyisi container içinde, cacerts ve keytool zaten orada mevcut olduğundan:
docker compose run --rm -v /host/ssl:/out:ro --entrypoint sh rest-api -c '
CACERTS=$(find / -name cacerts 2>/dev/null | head -1)
cp "$CACERTS" /tmp/work.p12
# switch store password from "changeit" to our value
keytool -importkeystore -noprompt \
-srckeystore /tmp/work.p12 -srcstorepass changeit \
-destkeystore /tmp/root.p12 -deststorepass <TRUSTSTORE_PW> -deststoretype PKCS12
# add our own CA
keytool -importcert -noprompt -alias corp-ca \
-file /out/corp-ca-root.pem \
-keystore /tmp/root.p12 -storepass <TRUSTSTORE_PW>
cat /tmp/root.p12
' > /host/installation/certs/root.p12
Doğrulama, hedefe ulaşıldığını gösterdi:
keytool -list -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> \ | grep -iE 'entries|corp-ca' # -> Your keystore contains 151 entries # -> corp-ca, ..., trustedCertEntry
151 girdi: ~150 public standart CA artı kendi dahili CA'm. Böylece servis her iki dünyaya da güveniyor. Ve gerçekten — bir sonraki başlatma çok beklenen satırı getirdi:
Tomcat started on port 8080 (http) with context path '/mw'
Ders: özel bir Java truststore varsayılanı genişletmez, onun yerini alır. Bir servis hem dahili hem de public hedeflere ulaşacaksa, truststore'u cacerts temel alınarak artı kendi CA'nızla oluşturun — yoksa kazara dünyanın yarısını dışarıda bırakmış olursunuz.
Son bir sınav gibi, nginx web sunucusu da takılıp kaldı:
nginx: [emerg] cannot load certificate "/etc/nginx/certs/issued.crt": PEM_read_bio_X509_AUX() failed ... wrong tag ... nested asn1 error
Deseni 1. bölümden tanıyorduk: çift Base64 kodlanmış bir dosya. Ama asıl ders farklı — nginx, Java servislerinden temelden farklı çalışır.
KANIT: Keycloak ve ara katman PKCS#12 konteynerlerini parola ile isterken, nginx düz açık metin PEM bekliyor: sertifika -----BEGIN CERTIFICATE----- blokları olarak, anahtar -----BEGIN PRIVATE KEY----- olarak — parola olmadan ve konteyner paketlemesi olmadan.
Neyse ki ikisini de zaten çalışan PKCS#12 sunucu keystore'undan çıkarabiliyordum:
# full chain (server certificate + CA) as PEM openssl pkcs12 -in issued.p12 -passin pass:<TRUSTSTORE_PW> -nokeys -out full_chain.pem # private key as unencrypted PEM openssl pkcs12 -in issued.p12 -passin pass:<TRUSTSTORE_PW> -nocerts -nodes -out private.key
Sondaki küçük engel: OpenSSL, PEM bloklarının önüne Bag Attributes düz metin satırları yazmayı seviyor; bazı nginx sürümleri bunlara takılıyor. Temizce kesersiniz:
sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' full_chain_raw.pem > full_chain.pem
Ve — neredeyse en önemlisi — sertifika ile anahtarın eşleşen bir çift olduğunu her zaman kontrol edin. İki hash özdeş olmalı:
openssl x509 -in full_chain.pem -noout -modulus | openssl md5 openssl rsa -in private.key -noout -modulus | openssl md5
HÜKÜM: farklılarsa okunabilir dosyalar var ama TLS yine de başarısız olur — bu hata yoksa ancak handshake sırasında ortaya çıkar.
Büyük çıkarım
Hata mesajlarıyla dolu bir günün ardından, özü bu tek projeden çok daha ötesine geçen birkaç ilkeye dökülebilir:
- "Sertifika" tek tip bir şey değildir. PEM, DER, PKCS#12, truststore, keystore, CA root, son sertifika — bunlar farklı kurallara tabi farklı şeyler. Kaybedilen zamanın büyük bölümü onları birbirine karıştırmaktan geliyor.
- Dosya uzantısı yalan söyler. Dosyanın gerçekte ne içerdiğini
file,head,openssl x509 -textvekeytool -listsöyler — adı değil. - Farklı araçların farklı gerçekleri vardır. OpenSSL'in bir dosyayı kabul etmesi Java'nın da kabul edeceği anlamına gelmez. Dosyayı nihayetinde kullanacak araçla doğrulayın.
- Hata mesajları ipucu, karar değildir. "Yanlış parola" aslında "yanlış algoritma" demekti. "Bağlantı reddedildi" aslında "servis çalışmıyor" demekti. Onları kelimesi kelimesine okuyun ve teknik olarak ne anlama geldiklerini sorgulayın.
- Değişiklikten sonra hiçbir şey değişmiyorsa, değişiklik yanlış yerdeydi. Bu tek farkındalık bana saatler kazandırırdı. Aynı onarımı tekrarlamadan önce yolu, dosyayı ve değeri önceden doğrulayın.
- Parmak iziyle doğrulayın, adla değil. Bir alias ya da dosya adı hiçbir şeyi kanıtlamaz. Bir sertifikanın parmak izi tek güvenilir kimliktir.
- Güven mimarisini anlayın, yalnızca komutları değil. Özel truststore varsayılanın yerini alır. Truststore, son sertifikayı değil CA'yı gerektirir. Farklı servisler farklı formatlar bekler. Modeli anlayan komutları zar zor ezberlemek zorunda kalır.
Sonunda stack'in tamamı çalışıyordu: auth sunucusu, iki güven çıpasıyla ara katman, web sunucusu. Tek bir .pem dosyasından birbiriyle etkileşen bir düzine container'a uzanan bir yolculuk — ve her engel, bir dahakine yalnızca dakikalar alacak bir şey öğretti.
Serinin sonu. Birlikte okuduğunuz için teşekkürler — ve bir sonraki sertifika hatanız öğretici biri olsun.