NSA Kubernetes Hardening Notları - 2. Bölüm

August 14, 2021

Merhaba,

İlk bölümde ağırlıklı olarak Pod güvenliğine ve Pod Security Policy kavramına değinmiştik. İkinci bölümde ilk olarak namespace kavramına değindikten sonra Network tarafını inceleyeceğiz.

Namespaces

Bir namespace, Kubernetes objelerini mantıksal olarak kümelere bölmemizi sağlar. Buradaki amaç, farklı takımların kendilerine ait kaynaklarını bir başka takımın erişememesidir. Elbette bu namespace haricinde farklı auuthentication yöntemleriyle de yapılabilir ama namespace bu izolasyonu sağlayabileceğimiz en pratik yoldur.

Namespace’ler kendi içlerinde de label yardımıyla bir kullanıcı veya Pod atamasına yönelik özelleştirilebilir. Varsayılan olarak üç namespace bulunur ve bu namespace’ler silinemez:

  • default (Kullanıcının oluşturduğu kaynakları tutar)
  • kube-public (Public kaynaklar tutulur)
  • kube-system (Kubernetes bileşenlerini içerir)

Ekran görüntüsünden aradaki farkı daha iyi anlayabilirsiniz.

9

default namespace’i hariç, oluşturulan herhangi bir nesne diğer iki varsayılan namespace’de tutulmamalıdır. Farklı namespace’ler içerisinde bulunan Pod ve(ya) servisler, herhangi bir engelleyici kural oluşturulmadığı sürece birbirleriyle iletişim kurabilirler.

Bir namespace oluşturmanın iki yolu vardır, birincisi tek komut olarak şu şekildedir:

kubectl create namespace namespace_adı

İkinci yol ise YAML dosyası kullanmaktır, örnek olarak:

apiVersion: v1
kind: Namespace
metadata:
  name: test

Yeni bir kaynak oluştururken namespace özelleştirmesi aynı şekilde metadata altında yapılabilir veya komut tarafında ise —namespace veya -n parametresiyle hedef namespace belirtilebilir.

10

Network Policies

Networking, birden çok uygulamanın veya nesnenin birbirleriyle haberleştiği hemen hemen her mimaride ayrı bir önem arz etmektedir. Kubernetes dünyasında ise Podların, containerların, master ve worker node bileşenlerinin birbirleriyle olan iletişimi sebebiyle bir saldırgan gözünden bakıldığında olası zayıf noktalar bir şekilde dikkat çekeceği için, cluster yöneticisi olarak network düzeyinde gerekli yapılandırmaları ve kuralları uygulamak oldukça önemlidir.

Bir network policy, cluster üzerindeki trafiğin kural bazında yönetilmesine olanak sağlar. Farklı bir aksiyon alınmadığı sürece, varsayılan olarak herhangi bir network policy uygulanmaz.

Bir policy oluşturabilmek için en önemli şart, NetworkPolicy API’nı destekleyen bir network plugin’in kullanılması gerektiğidir.

Not: Kullanılan CNI plugin’i türüne göre Network Policy’nin kullanım formatı farklılık gösterebilir.

Örneklere başlamadan önce Policy Type kavramına değinelim. Bir Network Policy oluştururken iki seçeneğimiz bulunur:

ingress: Gelen trafiği kontrol eder. egress: Çıkan trafiği kontrol eder.

Lakin birazdan örneklerde de göreceğiniz üzere her iki tip de aynı Network Policy içerisinde bulunabilir. Bu type’lar, from ve to parametreleriyle kuralın hangi Pod veya Pod’lar üzerinden geleceği veya hangi Pod veya Pod’lara uygulanacağı konusunu belirler.

Bir diğer önemli bileşen de podSelector parametresidir. podSelector, kuralların uygulanacağı Pod veya Pod’ları belirler.

Şimdi sık kullanılabilecek bazı senaryoları örnekleyelim.

Gelen tüm trafiğe izin verilmesi:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-ingress
spec:
  podSelector: {}
  ingress:
    - {}
  policyTypes:
    - Ingress

Gelen tüm trafiğin engellenmesi:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
    - Ingress

Giden tüm trafiğe izin verilmesi:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-egress
spec:
  podSelector: {}
  egress:
    - {}
  policyTypes:
    - Egress

Giden tüm trafiğin engellenmesi:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
spec:
  podSelector: {}
  policyTypes:
    - Egress

Son olarak, yine pratik olarak kullanılabilecek bir diğer senaryo olan hem gelen hem de giden trafiğin engellenmesine bakalım.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Tüm bu işlemlerde podSelector parametresinin {} olarak ayarlandığına dikkat edin, bu şekilde Pod seçimi spesifik olmaktan çıkıp cluster’daki tüm Pod’lara yönelik olacaktır.

Şimdi biraz farklı örnekler yapalım.

Mesela, TCP protokolü için tüm Pod’ların 80. port’a erişimlerini bloklayalım.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all-egress
spec:
  podSelector: {}
  egress:
    - to:
      ports:
        - protocol: TCP
          port: 80
  policyTypes:
    - Egress

TCP haricinde aynı şekilde UDP protokolü de eklenebilir.

  ports:
    - protocol: TCP
      port: 80
    - protocol: UDP
      port: 80

podSelector haricinde namespace üzerinden de hedef(ler) belirlenebilir. Mesela şu örneği inceleyelim:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all-egress
spec:
  podSelector: {}
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              namespace: test
          podSelector:
            matchLabels:
              app: ssh
      ports:
        - protocol: TCP
          port: 22
  policyTypes:
    - Egress

Buradaki kural tüm Pod’lara yönelik uygulanmıştır. Lakin hedef olarak sadece test namespace’i ve label’ı app=ssh olarak belirtilen Pod’lar seçilmiştir.

Bir başka örneği inceleyelim.

kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
  name: allow-egress-test
  namespace: test
spec:
  podSelector:
    matchLabels:
      app: matrix
  egress:
    - to:
        - ipBlock:
            cidr: 172.17.0.0/24

Bu örnekte ise hedef olarak CIDR belirtilmiştir. ipBlock parametresiyle 172.17.0.0/24 bloğunda bulunan Pod’lara giden trafiğe izin verilmiştir.

Son olarak her iki tipi de içeren bir örnekle bu bölümü sonlandıralım.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: den1z
  namespace: test
spec:
  podSelector:
    matchLabels:
      app: matrix
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - ipBlock:
            cidr: 172.18.0.0/24
        - namespaceSelector:
            matchLabels:
              name: allowedns
        - podSelector:
            matchLabels:
              app: allowedpod
      ports:
        - protocol: TCP
          port: 22
  egress:
    - to:
        - ipBlock:
            cidr: 10.0.7.0/24
        - namespaceSelector:
            matchLabels:
              name: targetns
        - podSelector:
            matchLabels:
              app: targetpod
      ports:
        - protocol: TCP
          port: 53

Ingress ve Egress aynı anda uygulanmış, Ingress için CIDR bloğu ve hedef Port olarak 22 verilerek namespace ve Pod label’ı özelinde kuralın kimlere uygulanacağı belirtilmiştir. Egress tarafında da aynı şekilde hem namespace hem Pod için selector kullanılmış, DNS portu belirtilerek sadece 10.0.7.0/24 bloğundaki Pod’lar kurala dahil edilmiştir.

Resource Policies

Namespace ve node düzeyinde limitlendirme işlemlerinde kullanılabilecek iki resource policy vardır:

  • LimitRange
  • ResourceQuota

LimitRange, herhangi bir namespace’deki Pod ve(ya) container’daki CPU ve disk kullanımlarını kısıtlamayı sağlar. Her bir namespace’e yönelik sadece bir LimitRange kısıtlayıcısı oluşturulabilir.

ResourceQuota ise namespace üzerindeki toplam CPU / RAM kullanımına yönelik kısıt oluşturmaya yarar. İki kısıtlayıcıyı da örneklemeden önce testlerde kullanmak üzere ‘test’ isminde yeni bir namespace oluşturalım:

kubectl create namespace test

Şimdi de cpu isminde bir dosya oluşturarak aşağıdaki gibi bir konfigürasyon sağlayalım. Minimum CPU isteğini 200 MB, maksimum 500 MB olarak belirleyelim.

apiVersion: v1
kind: LimitRange
metadata:
  name: cpu-min-max-demo
spec:
  limits:
    - max:
        cpu: "500m"
      min:
        cpu: "200m"
      type: Container

k apply -f cpu --namespace=test

Not: k komutunu kubectl’in alias’ı olarak atadığım için örneklerde kestirme olarak bu şekilde kullanıyorum. Siz de aynı şekilde kullanmak istiyorsanız aşağıdaki komutu girebilirsiniz:

alias k=kubectl

Bu kuralı namespace özelinde spesifik hale getirdiğimize dikkat edin. Şimdi aynı test namespace’i üzerinde CPU limitlerini aşarak yeni bir Pod oluşturmayı deneyelim.

apiVersion: v1
kind: Pod
metadata:
  name: constraints-cpu-demo
  namespace: test
spec:
  containers:
  - name: constraints-cpu-demo
    image: nginx
    resources:
      limits:
        cpu: "900m"
      requests:
        cpu: "700m"

4

Gördüğünüz gibi maximum CPU limitini 500 MB olarak ayarladığımız için, Pod tarafında 900M limitini kullanmamıza izin vermedi ve Pod’u oluşturmadı. Pod limitini 499M olarak, requests limitini de 400M şeklinde güncelleyelim ve tekrar deneyelim.

k describe po ... komutu ile incelediğimizde limitleri görebiliyoruz.

5

Şimdi de ResourceQuota kullanarak bir namespace üzerindeki tüm containerların kullanabileceği toplam CPU ve RAM miktarını kısıtlayan bir örnek yapalım. İlk olarak ResourceQuota nesnesini oluşturalım ve önceden oluşturduğumuz namespace’i kullanalım.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: mem-cpu-test
  namespace: test
spec:
  hard:
    requests.cpu: "1"
    limits.cpu: "2"
    requests.memory: 700M
    limits.memory: 1Gi

6

Şimdi de iki ayrı Pod konfigürasyonu oluşturalım ve limitler toplamının oluşturduğumuz kuralı geçmesine göre ayarlayalım.

1. Pod

apiVersion: v1
kind: Pod
metadata:
  name: quota-mem-cpu-test
  namespace: test
spec:
  containers:
    - name: quota-mem-cpu-test
      image: nginx
      resources:
        limits:
          memory: "400Mi"
          cpu: "400m"
        requests:
          memory: "400Mi"
          cpu: "400m"

2. Pod

apiVersion: v1
kind: Pod
metadata:
  name: quota-mem-cpu-test2
  namespace: test
spec:
  containers:
    - name: quota-mem-cpu-test2
      image: nginx
      resources:
        limits:
          memory: "700Mi"
          cpu: "700m"
        requests:
          memory: "700Mi"
          cpu: "700m"

Şimdi beklentimiz şu; bu Pod’ları ayrı ayrı oluşturmamız durumunda herhangi bir hata almamamız gerekiyor çünkü kendilerine ait olan limit değerleri kuralı geçmiyor, fakat biz art arda oluşturmayı deneyerek hata almayı bekleyeceğiz.

7

Evet, tam olarak istediğimiz şekilde 2. Pod’u oluştururken hata aldık. Burada dipnot edilmesi gereken nokta, az önce oluşturuduğumuz LimitRange nesnesinin öncelikli olarak çalıştığıdır, yani ResourceQuota sınırlamasına gelmeden önce şayet talep edilen kaynak miktarı LimitRange kuralının üzerindeyse hata alırsınız, bu yüzden:

k delete -f dosya_adı -n test

komutu ile LimitRange’i silebilirsiniz.

Bu başlıkta önerilen aksiyonları özetlemek gerekirse:

  • NetworkPolicy API’nı destekleyen bir CNI plugin’i kullanın.
  • Policyleri podSelector ve(ya) namespaceSelector ile uygulayın.
  • Gelen ve giden trafiği engellemek için bir ‘default’ policy kullanın.
  • Bir namespace veya Pod düzeyinde kaynak limitlemek için LimitRange ve ResourceQuota policylerini kullanın.

kube-apiserver Güvenliği

kube-apiserver, bir önceki bölümde de değindiğimiz gibi çekirdek bileşenlerden birisi ve yanlış - eksik yapılandırmadan dolayı API üzerinden saldırganların cluster erişimini ele geçirmesi, potansiyel atak senaryolarından sadece bir tanesidir.

Bir önceki yazıda da bahsettiğim cryptojacking saldırısıyla yaklaşık 3.5 sene önce Tesla’nın başı bu konuda yandığı için, bilmeyenlerin ilgisini çeker diye kaynak paylaşıyorum:

Tesla

Yine önceki yazıda da belirttiğim CIS’in yayınladığı benchmark tavsiyelerini kube-apiserver üzerinde uygulayabilirsiniz.

Bu işlemleri uygulamak için /etc/kubernetes/manifests/kube-apiserver.yaml dosyasını uygun şekilde değiştirdikten sonra yeni konfigürasyonun uygulanması için kubelet servisini yeniden başlatabilirsiniz.

CIS No Title Varsayılan
1.2.1 —anonymous-auth parametresi kullanılmamalı ya da ‘false’ olarak set edilmelidir Aktif
1.2.2 —basic-auth-file parametresi kullanılmamalıdır Aktif
1.2.3 —token-auth-file parametresi kullanılmamalıdır Aktif
1.2.4 —kubelet-https parametresi kullanılmamalı ya da ‘true’ olarak set edilmelidir Aktif
1.2.5 –-kubelet-client-certificate ve —kubelet-client-key parametreleri uygun şekilde set edilmelidir Aktif
1.2.6 -–kubelet-certificate-authority parametresi uygun şekilde set edilmelidir Pasif
1.2.7 –-authorization-mode parametresi AlwaysAllow olarak set edilmemelidir Aktif
1.2.8 -–authorization-mode parametresi Node’u kapsamalıdır Aktif
1.2.9 -–authorization-mode parametresi RBAC’ı kapsamalıdır Pasif
1.2.10 Admission control plugin’i EventRateLimit set edilmelidir Pasif
1.2.11 Admission control plugin’i AlwaysAdmit kullanılmamalıdır Pasif
1.2.12 Admission control plugin’i AlwaysPullImages kullanılmamalıdır Pasif
1.2.13 PSP kullanılmıyorsa, admission control plugin’i SecurityContextDeny set edilmelidir Pasif
1.2.14 Admission control plugin’i ServiceAccount set edilmelidir Pasif
1.2.15 Admission control plugin’i NamespaceLifecycle set edilmelidir Pasif
1.2.16 Admission control plugin’i PodSecurityPolicy set edilmelidir Pasif
1.2.17 Admission control plugin’i NodeRestriction set edilmelidir Aktif
1.2.18 —insecure-bind-address parametresi kullanılmamalıdır Aktif
1.2.19 -–insecure-port parametresi 0 olarak set edilmelidir Aktif, 0
1.2.20 -–secure-port parametresi 0 olarak kullanılmamalıdır Aktif, 6443
1.2.21 -–profiling argument parametresi ‘false’ olarak set edilmelidir Pasif
1.2.22 ––audit-log-path parametresi set edilmelidir Pasif
1.2.23 -–audit-log-maxage parametresi ‘30’ veya bir başka uygun değere set edilmelidir Pasif
1.2.24 -–audit-log-maxbackup ‘10’ veya bir başka uygun değere set edilmelidir Aktif
1.2.25 —audit-log-maxsize ‘100’ veya bir başka uygun değere set edilmelidir Aktif
1.2.26 -–request-timeout parametresi uygun bir değere set edilmelidir Aktif
1.2.27 -–service-account-lookup parametresi ‘true’ olarak set edilmelidir Pasif
1.2.28 —service-account-key-file parametresi uygun bir değere set edilmelidir Aktif
1.2.29 ––etcd-certfile ve —etcd-keyfile parametreleri uygun değerlere set edilmelidir Aktif
1.2.30 ––tls-cert-file ve -–tls-private-key-file parametreleri uygun değerlere set edilmelidir Aktif
1.2.31 ––client-ca-file uygun bir değere set edilmelidir Aktif
1.2.32 ––etcd-cafile argument uygun bir değere set edilmelidir Aktif
1.2.33 ––encryption-provider-config parametresi uygun bir değere set edilmelidir Pasif
1.2.34 Encryption sağlayıcıları konfigüre edilmelidir Pasif
1.2.35 API Server sadece güçlü kriptografik cipher’lar kullanmalıdır Pasif

etcd Güvenliği

etcd, cluster’a ait durum bilgisini ve secret’ları muhafaza eder, bu durum da kendisini güvenlik açısından son derece önem arz eden bir konuma getirir. Bir şekilde etcd erişimini ele geçiren bir saldırgan, tüm cluster düzeyinde yetki sağlayabilir ki böyle bir durumda secret’ları ve diğer hassas verileri de okuyabilir. etcd’nin sadece API server üzerinden bir yetkilendirme mekanizması doğrultusunda erişilebilmesi önerilmektedir. Alınabilecek aksiyonlardan birisi, etcd ve API server arasındaki iletişimin TLS sertifikaları ile HTTPS protokolüne zorunlu olarak yönlendirilmesidir.

Kubernetes, varsayılan kurulumda etcd için gerekli olan TLS encryption’ı sağlamaktadır. Process kontrolü ile üç parametreyi kontrol etmeniz gereklidir:

  • —cert-file=…
  • —client-cert-auth=…
  • —key-file=…

ps aux | grep etcd komutuyla kontrol sağlayalım.

1

Görüldüğü gibi etcd için TLS sağlanmış durumda. (Görüntüyü yeni sekmede açıp kontrol edebilirsiniz, some resolution problems…)

Not: Benim örnekte kullandığım cluster Minikube ile oluşturulduğu için, /var/lib/minikube/… dizinini göstermektedir. kubeadm veya bir başka araç ile oluşturulan cluster’lar için bu dizinlerin yolu farklılık gösterebilir.

Bu noktada Kubernetes’in önerdiği ek güvenlik önlemlerinden birisi de verilere encryption uygulamaktır. Varsayılan ayarlarda bu güvenlik katmanı uygulanmaz, kube-apiserver parametrelerini kontrol ederek —encryption-provider-config parametresinin bulunmadığını görebilirsiniz.

Şimdi bir EncryptionConfiguration nesnesi oluşturarak ilgili parametreyi bu nesneyi işaret edecek şekilde set edeceğiz.

İlk olarak 32 bytelık bir random anahtar oluşturarak base64 ile encode edelim.

head -c 32 /dev/urandom | base64

Çıkan değeri kullanarak bir konfigürasyon oluşturalım. Ben /mnt dizini altında encry.yaml adında bir dosya oluşturdum.

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
    - secrets
    providers:
    - aescbc:
        keys:
        - name: key
          secret: QsGpzJCzuuBXhw8/6QtmRGujD4VAcsnyzE8Lhakfhn0=
    - identity: {}

Şimdi oluşturduğumuz bu yapılandırmayı —encryption-provider-config parametresiyle kube-apiserver üzerinde set edelim. Bunun için ilk olarak /etc/kubernetes/manifests/kube-apiserver.yaml dosyasını açıyoruz.

8

—encryption-provider-config=…

parametresini dosyayı oluşturduğunuz dizine set ettikten sonra API server’ı yeniden başlatıyoruz. Bunun için kubelet servisini yeniden başlatabilir veya kube-apiserver.yaml dosyasını silebilirsiniz, otomatik olarak tetiklenecektir.

kubeconfig dosyasının Güvenliği

Daha önce Kubernetes ile herhangi bir şekilde vakit geçirdiyseniz, kubeconfig dosyasının cluster’ın temel yapıtaşlarından birisi olduğunu zaten fark etmişsinizdir. Bu dosya, cluster veya cluster’lara ait yetkilendirme parametreleri, kullanıcı, endpoint, namespaces gibi önemli bilgileri içerir ve varsayılan olarak kullanıcının ev dizini altındaki .kube klasöründe tutulur.

Ek bir güvenlik önleminin alınmadığı bir senaryoda, kubeconfig dosyasını ele geçirmiş bir saldırgan, cluster’a erişim sağlayabilir. Bu durumda konu Kubernetes özelinden çıkıp belki biraz daha Linux tarafında değerlendirilebilir fakat Cloud sağlayıcıların da kubeconfig dosyasının yetkilendirilmesiyle ilgili sundukları çözümler bulunmaktadır. Olaya Linux tarafından baktığımızda, en basitinden chmod ile dosya ve dizin yetkileri kontrol edilebilir.

TLS Bootstrapping

Control plane güvenliğindeki aşamalardan birisi de TLS Bootstrapping’tir. Bir cluster’ın TLS 1.2 ya da TLS 1.3 ile birlikte kullanılması önerilmektedir. Bu işlemleri bu yazıda göstermeyeceğim ama kaynak olarak Kubernetes’in dokümantasyonunu veya Kubernetes The Hard Way reposunu inceleyebilirsiniz.

Secrets

Bir secret; parola, OAuth token, SSH anahtarı gibi nesnelerin bilgilerini tutar. Hassas verileri bir YAML konfigürasyonunda, Dockerfile içerisinde veya çevresel değişken olarak plain text şeklinde saklamaktansa, secret nesnesini kullanırız. Varsayılan olarak secret bilgileri base64 olarak encode edilmiş string’ler şeklinde tutar. Bu bağlamda secret erişimlerinin RBAC ile yetkilendirilmesi veya secret’lara yönelik encryption uygulanması önerilir.

Encryption; API server üzerinden veya cloud sağlayıcılarında da bulunan bir Key Management Service (KMS) ile sağlanabilir.

API Server örneği için etcd başlığındaki işlemi takip edebilirsiniz. KMS ise hemen hemen aynı şekilde yapılır, aradaki fark olarak provider’ı kms’i kullanarak endpoint & cachesize & timeout parametrelerini belirtmek gerekir.

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - kms:
          name: myKmsPlugin
          endpoint: unix:///tmp/socketfile.sock
          cachesize: 100
          timeout: 3s
      - identity: {}

Aynı şekilde —encryption-provider-config parametresi API Server için set edildikten sonra yeniden başlatılması gerekir.

  1. bölümde görüşmek üzere.


Written by Deniz Parlak