Tahmin edebileceğimiz gibi günümüz ihtiyaçlarının hızla artmasıyla beraber, bir çok organizasyon bu ihtiyaçlara ayak uydurabilmek için teknolojisini hızla yenilemeye devam ediyor. Buradaki yaklaşım ise genellikle hem business’ı/organizasyonu hem de teknolojiyi scale edebilmek için microservice architecture’ı odaklı oluyor.
Bildiğimiz gibi bu yenileme süreci içerisinde bir çok dokunmamız gereken konular/alanlar mevcut. Bana göre en kritik konulardan birisi, microservice’ler arası iletişimin güvenli bir hale getirilmesi.
Sonuçta hiç birimiz hassas verilerimizin kolaylıkla ele geçirilebilmesini istemeyiz, değil mi?
Bu makale kapsamında ise “microservice’ler arası iletişimi, Istio Service Mesh ile nasıl güvenli bir hale getirebiliriz” konusuna değinmeye çalışacağım.
Bildiğimiz gibi microservice ekosistemi içerisinde istio gibi bir service mesh teknolojisi kullandığımızda, uygulamalarımız arası iletişimi service mesh doğası gereği ele almaktadır.
Böylece uygulama tarafında herhangi bir kod değişikliği yapmadan reliability, discovery ve monitoring gibi alanlarda istio’dan yararlanabilmekteyiz. Bunların yanı sıra microservice’ler arası güvenli iletişimi sağlayabilmemiz için de bizlere farklı çözümler sunmaktadır.
Microservice’lerimiz her ne kadar güvenli olarak kabul ettiğimiz internal ortamlarımızda çalışıyor olsa da, microservice’ler arası iletişimi encrypted bir hale getirmek güvenlik açısından her türlü avantajımıza olacaktır.
Güvenlik kapsamında istio, iki farklı authentication yöntemini desteklemektedir.
- Service-to-service iletişimi için transport authentication (mTLS).
- Client-to-service iletişimi için ise JWT ile end-user authentication yöntemi.
Mutual TLS ile Service-to-Service İletişim Güvenliğini Sağlamak
Az önce de bahsettiğimiz gibi, kod tarafında herhangi bir değişiklik yapmadan microservice’ler arası güvenli iletişimi istio ile sağlayabilmekteyiz. Istio proxy, herhangi bir sertifikayı yönetmemize gerek kalmadan, 443 port’u üzerindeki trafiği bizim için yönetip, uygulama’nın 80 port’una yönlendirmektedir.
Istio bu tarz işlemler ve network’ü intercept edebilmek için, aşağıdaki diyagramdan da görebileceğimiz gibi Envoy‘un sidecar proxy’sini kullanmaktadır.
Mutual TLS authentication, client ve server olmak üzere her iki yönde de trafiğin hem güvenli hem de güvenilir olmasını sağlamaktadır.
Bu akışı ise kabaca özetlemek gerekirse;
Bildiğimiz gibi bir service bir trafik aldığında veya gönderdiğinde, service mesh’in doğası gereği bu trafik her zaman önce ilgili service’in local sidecar proxy’sinden geçer. Dolayısıyla mTLS kullanılarak bir request atıldığında, istio bu trafiği client’ın local sidecar’ına yönlendirmektedir. Bu local sidecar trafiği aldığında ise, server’ın sidecar’ı ile bir mTLS handshake işlemi gerçekleştirmeye başlar.
Eğer bu doğrulama işlem başarılı gerçekleşirse, client’ın sidecar’ı bu trafiği encpyt eder ve server’ın sidecar’ına gönderir. Server sidecar’ı ise bu trafiği decrypt ederek, trafiği gitmesi gereken noktaya yönlendirir.
Ek olarak istio, sertifikaların oluşturulması ve yönetilmesi işlemini de bizler için pürüzsüz bir şekilde gerçekleştirmektedir. Böylece operasyonel yüklerimizi de azalmaya yardımcı olmaktadır.
Örnek Bir Uygulama Deploy Edelim
Örnek gerçekleştirebilmemiz için .NET Core ile ürün ve stok bilgilerini dönen basit birkaç API geliştirdim. İlk olarak geliştirmiş olduğum “Product“, “Stock” ve “Product Gateway” API’larını kubernetes cluster’ına deploy edeceğiz. Ardından istio mesh ile bu API’lar arasında gerçekleşecek olan iletişimi mTLS ile güvenli bir hale getireceğiz.
Örneğimizde “Product Gateway” API’ının, GET “/products/1” endpoint’ini kullanacağız. Bu endpoint kısaca aşağıdaki logic’i içermektedir ve basit olarak bir ürün response’u dönmektedir.
[HttpGet("{id}")] public async Task Get(int id) { HttpClient httpClient = _httpClientFactory.CreateClient(); string productAPIUrl = _configuration.GetValue("productAPIUrl"); string stockAPIUrl = _configuration.GetValue("stockAPIUrl"); productAPIUrl = $"{productAPIUrl}/products/{id}"; stockAPIUrl = $"{stockAPIUrl}/stocks?productId={id}"; ; Task productResponse = httpClient.GetAsync(productAPIUrl); Task stockResponse = httpClient.GetAsync(stockAPIUrl); await Task.WhenAll(productResponse, stockResponse); if (productResponse.Result.IsSuccessStatusCode && stockResponse.Result.IsSuccessStatusCode) { var jOption = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; using var productResponseContent = await productResponse.Result.Content.ReadAsStreamAsync(); ProductDTO product = await JsonSerializer.DeserializeAsync(productResponseContent, jOption); using var stockResponseContent = await stockResponse.Result.Content.ReadAsStreamAsync(); StockDTO stock = await JsonSerializer.DeserializeAsync(stockResponseContent, jOption); var aggregatedProduct = new ProductAggregatedDTO { Id = product.Id, Name = product.Name, Quantity = stock.Quantity }; return Ok(aggregatedProduct); }; return NotFound(); }
Örnek kodlara ise buradan ulaşabilirsiniz.
Gereksinimler ve Varsayımlar
- Kubernetes cluster’ına ve temel containerization konsept bilgisine sahip olmak.
- Temel service mesh konsept bilgisine sahip olmak.
Uygulamaları deploy etmeye başlamadan önce, istio sevice mesh’i kubernetes cluster’ına kurmamız gerekmektedir.
Farklı platform’lara göre kurulum işlemlerini buradaki adımları takip ederek gerçekleştirebilirsiniz. Ben bu makale kapsamında Docker Desktop ve Istio 1.7.3 kullanacağım.
Başarılı bir kurulum işleminden sonra da, aşağıdaki gibi bir sonuç görüyor olmalıyız.
Son olarak otomatik sidecar injection işlemini gerçekleştirebilmemiz için, istediğimiz bir namespace’e aşağıdaki gibi bir label eklememiz gerekmektedir.
Ben burada “default” namespace’ini kullanacağım. Böylece bu namespace altında konumlanacak olan uygulamalar, otomatik olarak mesh içerisine dahil olacaklardır.
kubectl label namespace default istio-injection=enabled
Şimdi örnek API’ların deployment işlemlerine başlayabiliriz.
Ilk olarak image’leri aşağıdaki komut yardımıyla oluşturalım.
docker build -f ProductAPI/Dockerfile . -t ecom-sample-product-api:v1 docker build -f StockAPI/Dockerfile . -t ecom-sample-stock-api:v1 docker build -f ProductGatewayAPI/Dockerfile . -t ecom-sample-product-gateway-api:v1
Ardından burada hazırlamış olduğum “deploy.yaml” dosyasını kullanarak, aşağıdaki gibi deployment işlemlerini tamamlayalım.
kubectl apply -f deploy.yaml
Deployment işlemleri başarıyla tamamlandıktan sonra ise, pod’ların sidecar’ları ile beraber 2/2 “Running” statüsüne geçene kadar bekleyelim.
Ayrıca aşağıdaki komut yardımıyla ilgili pod’un mesh içerisine dahil olup olmadığını da kontrol edebiliriz.
istioctl x describe pod product-gateway-api-v1-7996bf7cdf-dz289
API’ların sorunsuz bir şekilde çalıştıklarını test edebilmek için ise, herhangi bir pod üzerinden aşağıdaki gibi “Product Gateway” API’ın “/products/1” endpoint’ine bir GET isteğinde bulunalım.
kubectl exec "$(kubectl get pod -l app=stock-api -o jsonpath={.items..metadata.name})" -c stock-api -it sh curl http://product-gateway-api.default.svc.cluster.local/products/1
Yukarıdaki resimden de görebileceğimiz gibi, “Product Gateway” API’ına HTTP üzerinden başarıyla erişebildik ve ürün bilgisi sonucunu alabildik.
API’lar problemsiz çalıştığına göre şimdi bu iletişimi end-to-end olarak mTLS ile güvenli bir hale getirebiliriz.
Genel olarak, az önce HTTP üzerinden gerçekleştirmiş olduğumuz işlemi de güvenli olarak kabul edebiliriz. Çünkü istio, 1.5 versiyonundan bu yana auto mTLS özelliği etkin ve permissive mode olarak gelmektedir.
Peki bunu nasıl özelleştirebiliriz? Haydi biraz daha detaya girelim.
Istio içerisinde mTLS seviyelerini uygulayabileceğimiz üç farklı nokta bulunmaktadır.
- Mesh seviyesinde
- Namespace seviyesinde
- Service seviyesinde
Bu seviyeler en küçüğe doğru birbirlerini override ederek gitmektedir. Örneğin farklı namespace’ler altında veya aynı namespace’de farklı gereksinimlere ihtiyaç duyan uygulamalarımız olabilir. Kısacası gereksinimlerimiz doğrultusunda dilediğimiz seviyede mTLS işlemlerini gerçekleştirebiliriz. Bu konu hakkındaki detaylı bilgiye ise buradan erişebilirsiniz.
Ben “default” namespace’i kapsamında mTLS’i aşağıdaki gibi özelleştirerek etkinleştireceğim. Bu işlemden beklentimiz ise, uygulamalarımız üzerinde herhangi bir değişiklik yapmadan uygulamalarımızın “default” namespace’i altında sadece mTLS trafiğini kabul etmelerini sağlamak olacak.
Bu işlemi yapabilmek için ise aşağıdaki gibi istio’nun “PeerAuthentication” custom resource’unu kullanacağız.
apiVersion: "security.istio.io/v1beta1" kind: "PeerAuthentication" metadata: name: "default" namespace: "default" spec: mtls: mode: STRICT
Mutual TLS’i “default” namespace’i kapsamında etkinleştireceğimiz için, “metadata” altında bulunan “namespace” element’inin değerini “default” olarak set etmemiz gerekmektedir.
Ayrıca STRICT, PERMISSIVE ve DISABLED olmak üzere üç farklı kullanabileceğimiz mTLS mode’u bulunmaktadır. Auto mTLS özelliğinin ise default olarak permissive mode’da geldiğinden bahsetmiştik. Biz örnek gereği mTLS’i zorunlu kılacağımız için, “STRICT” olan modu kullanacağız.
Encrypted trafiğin yanında plain-text trafiğe de izin vermek istiyorsak, “PERMISSIVE” modu’u da kullanabiliriz. Çünkü karşılaşılan en büyük problemlerden birisi, henüz service mesh ekosistemine adapte olmamış uygulamalar için trafiği kesmek ve bu süreci daha karmaşık bir hale getirmektedir. Özellikle sizde bizim gibi hem Windows hem de Linux container’lar ile çalışıyorsanız, karşılaşılması kaçınılmaz bir durum haline geliyor.
Kısacası “PERMISSIVE” modu’u kullanarak, sistem içerisine yeni migrate olmaya çalışan uygulamaların, bu migration süreçlerini daha pürüzsüz bir hale getirebiliriz.
Bunların yanı sıra mTLS’i aşağıdaki gibi port seviyesinde de özelleştirebilmek mümkündür.
portLevelMtls: 8080: mode: DISABLE
Peki, mTLS’i zorunlu kılmak istediğimiz için yukarıdaki yaml dosyasını kubernetes üzerinde aşağıdaki gibi apply edelim.
kubectl apply -f mtls.yaml
Şimdi uygulamış olduğumuz mTLS zorunluluğunun geçerli olup olmadığını test edelim. Bunun için mesh içerisine dahil olmayan bir test pod’una ihtiyacımız var.
Test işlemini gerçekleştirebilmek için ise aşağıdaki gibi “test” namespace’i altında istio tarafından yönetilmeyecek olan bir test pod’u oluşturalım ve ardından “Product Gateway” API’ına tekrardan HTTP üzerinden bir istek atmaya çalışalım.
kubectl run -i --tty -n test --rm test --image=curlimages/curl:7.71.1 --restart=Never -- sh curl http://product-gateway-api.default.svc.cluster.local/products/1
Gördüğümüz gibi bu sefer “Product Gateway” API’ına HTTP üzerinden erişemedik. Çünkü “default” namespace’i altında bulunan uygulamalarımız, artık sadece mTLS trafiğini kabul etmektedirler.
Wohho! Uygulamalarımız artık bir tık daha fazla güvenli durumda.
Peki bu mTLS işlemleri arkaplanda nasıl yapılıyor. Sıkıcı kısım için hazır mısınız?
Çok fazla detaya girmeden kısaca inceleyelim.
Bu işlemleri daha net anlayabilmemiz için, envoy proxy’nin “Listener” ve “Cluster” configuration’larının detaylarını incelemeliyiz.
- Listener için kabaca downstream request’ler için proxy configuration’larıdır diyebiliriz.
- Cluster için ise kabaca upstream request’lerden sorumlu proxy configuration’larıdır diyebiliriz.
Listener Tarafı
İlk olarak bir pod üzerindeki listener’ları aşağıdaki gibi listeleyelim. Ben burada “product-gateway-api” pod’unu kullanacağım.
istioctl pc listeners product-gateway-api-v1-7996bf7cdf-dz289
Yukarıdaki resimde de görebileceğimiz üzerine bir çok listener bulunmakta. Bizim ise burada ilgilenecek olduğumuz “15006” portu. İlk olarak bu listener bir pod’a gelen tüm inbound trafiği almaktadır. Yani “product-gateway-api” pod’una gelen tüm trafik, önce bu listener tarafından handle edilmektedir.
Bir listener temel olarak içerisinde çeşitli configuration’lar ve gelen request’ler için farklı filter’lar bulundurmaktadır. Yukarıdaki resimden de görebileceğimiz üzere, bu filter’lar bir “filterChains” elementi altında toplanmaktadır. Bu noktada bizim inceleyecek olduğumuz filter ise, TLS’in etkin olduğunda devreye girecek olan filter.
Öncelikle aşağıdaki komut yardımı ile inbound listener detaylarını elde edelim.
istioctl pc listeners product-gateway-api-v1-7996bf7cdf-dz289 --address 0.0.0.0 --port 15006 -o json | less
Bu noktada ise, oldukça uzun bir response alacağız. Fakat ben sadece gerekli gördüğüm kısımlara değineceğim.
İlgilenecek olduğumuz filter’ın, TLS etkin olduğunda devreye girecek olan filter olduğunu söylemiştik.
Bu filter’ı ise aşağıdaki gibi ayırt edebiliriz.
"filterChains": [ { "filterChainMatch": { "prefixRanges": [ { "addressPrefix": "0.0.0.0", "prefixLen": 0 } ], "transportProtocol": "tls" },
Bu filter içerisindeki bizim için önemli olan noktalardan bir tanesi “transportSocket” element’i. Bu element içerisinde TLS için gerekli olan sertifika gibi bilgilerin yer aldığını görebiliriz.
Biraz daha detaya girmek gerekirse, “tlsCertificateSdsSecretConfigs” element’ine baktığımızda TLS sertifikalarının çekilebilmesi için gerekli olan Secret Discovery Service (SDS) bilgilerinin burada yer aldığını görebiliriz. Ayrıca “validationContextSdsSecretConfig” element’inde ise, TLS sertifikalarının doğrulama işlemleri için gerekli olacak “ROOTCA” erişim bilgilerinin de yer aldığını görebiliriz.
Mutual TLS mesh içerisinde aktif olduğu için, “ROOTCA” erişim bilgileri doğrulama işlemleri için zorunludur. Ayrıca bu gerekliliği “requireClientCertificate” element’inin değerine bakarak da anlayabiliriz. Bizim senaryomuzda “true” olduğunu da görebiliriz. Yani “product-gateway-api” pod’una istek yapan bir client, erişim sağlayabilmesi için kendi sertifikasını da sunması gerekmektedir.
Client kendi sertifikasını sunduğunda ise burada devreye envoy’un TLS Inspector filter’ı devreye girer. Bu filter, SNI‘ın elde edilmesi gibi initial TLS handshake işlemlerini gerçekleştirir. Böylece bu SNI bilgisi, “filterChains” eşleştirme işlemleri için kullanılabilir bir hale gelir. Bu konu hakkında daha detaylı bilgilere ise, buradan erişebilirsiniz.
Cluster Tarafı
Şimdi ise client tarafındaki işlemlere bir bakalım. Bunun için ise upstream request’in configuration’larına bakmamız gerekmektedir.
İlk olarak aşağıdaki komut yardımıyla “product-gateway-api” pod’unun cluster özetini elde edelim.
Burada görebileceğimiz üzere bir çok endpoint bulunmakta. Biz ise örnek amaçlı “product-gateway-api” pod’undan “stock-api” pod’una erişmeye çalışılıyorken kullanılan configuration detaylarına bir bakalım.
Bunu yapabilmek için aşağıdaki komut yardımıyla “stock-api.default.svc.cluster.local” endpoint’inin detaylarını elde edeleim.
istioctl pc cluster product-gateway-api-v1-7996bf7cdf-dz289 --fqdn stock-api.default.svc.cluster.local -o json | less
Burada da listener’da olduğu gibi “transportSocket” element’inin TLS sertifika işlemleri için yer aldığını görebiliriz. Ayrıca TLS işlemleri için kullanılacak olan “sni” bölümü de burada yer almaktadır.
Toparlayalım
Hiçbirimiz hassas verilerimizin kolayca elde edilebilmesini istemeyiz. Bu sebeple uygulamalarımız arası iletişimin güvenli bir şekilde gerçekleşiyor olması, oldukça önemli bir konu. Bu makale kapsamında ise kod tarafında herhangi bir değişiklik yapmadan, istio service mesh’i kullanarak uygulamalarımız arası iletişimi güvenli bir hale getirmeye çalıştık. Istio ile mTLS’i global olarak etkinleştirebilmenin yanı sıra, farklı scope’lara göre de configure edebilmemize izin veriyor olması oldukça harika bir esneklik. Ayrıca istio sertifikaların yönetilmesi açısından da, bizleri bir çok operasyonel yükten kurtarmaktadır.
Referanslar
https://istio.io/latest/docs/reference/config/security
https://istio.io/latest/docs/ops/diagnostic-tools/proxy-cmd/
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/transport_socket/transport_socket
https://developer.ibm.com/technologies/containers/tutorials/istio-security-mtls/