[Kubernetes in Action] Securing Cluster nodes and the network

2022. 2. 2. 08:18DevOps/Docker & Kubernetes

 

 

 

Using the host node's namespace in a pod

파드 안에서 동작하는 컨테이너들은 별도의 Linux Namespace 아래에서 동작합니다. 이는 다른 컨테이너의 프로세스나 노드의 default 프로세스로부터 해당 컨테이너의 프로세스를 격리시킵니다.

 

각각의 파드는 자신의 고유한 네트워크 Namepsace를 가져야 하기 때문에 자신만의 고유한 IP와 포트 Space를 갖습니다. 또한 자신만의 고유한 Process Tree를 가지며 자신만의 IPC Namespace를 사용합니다. (다른 파드와 통신할 때는 IPC를 사용해서만 통신이 가능합니다.) 이렇게 쿠버네티스는 특정 파드에 네트워크, Process, IPC 공간을 별도로 할당하고 다른 파드들과 철저하게 격리시키는 구조를 가지고 있습니다.

 

 

Using the node's network namespace in a pod

시스템 파드(노드 레벨의 자원을 수정하는 등의 역할을 하는)의 경우 노드(Host)의 Default Namespace에서 동작해야 합니다. 이는 hostNetwork의 값을 true로 설정하여 파드를 실행하면 됩니다. 이렇게 하면 파드는 별도의 네트워크 namespace를 갖지 않고 호스트의 네트워크 어댑터를 사용하게 됩니다.

 

호스트의 네트워크 어댑터를 사용하게 되면 해당 파드는 노드(Host)의 네트워크 인터페이스에 접근할 수 있게 되고, 파드에 별도의 IP주소가 할당되지 않습니다. 파드의 특정 포트에 바인드된 프로세스는 파드의 포트 = 노드의 포트 이므로 노드의 포트에 bind 됩니다. 

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-host-network
spec: 
  hostNetwork: true
  containers:
  - name: main
    image: alpine
    command: ["/bin/sleep", "999999"]

 

 

실제로 Kubernetes Contorl Plane에 Deploy된 파드들은 이렇게 hostNetwork 옵션을 사용하여 파드 안에서 동작하지 않는 것처럼 행동하도록 만듭니다.

 

 

Binding to a host port without using the host's network namespace

위에서 살펴본 hostNetwork를 사용하면 파드에 별도의 네트워크 Namespace를 할당하지 않고 파드가 자신의 호스트 노드의 네트워크 어댑터를 사용하도록 할 수 있습니다. 파드가 자신의 노드의 특정 포트와 Bind 되면서도 자신만의 네트워크 Namepsace를 사용하도록 하기 위해서는 hostPort를 사용하면 됩니다. hostPort는 말 그대로 호스트의 네트워크 인터페이스 전체가 아닌 "특정 포트만 사용하겠다"라는 의미이므로 파드가 자신의 네트워크 Namespace를 갖되, 호스트 노드의 특정 포트와 해당 파드가 직접 Bind 되어 연결되는 것이며 container의 ports 설정에 명시하여 사용할 수 있습니다.

 

apiVersion: v1
kind: Pod
metadata:
  name: yeoul-hostport
spec:
  containers:
  - image: luksa/kubia
    name: yeoul
    ports:
    - containerPort: 8080
      hostPort: 9000
      protocol: TCP

 

hostPort는 쿠버네티스 서비스에서 제공하는 "NodePort"와는 다릅니다. hostPort 옵션을 사용하면 해당 옵션을 사용한 파드가 있는 노드의 특정 포트에만 영향을 미치기 때문에 해당 옵션을 사용한 파드가 없는 노드의 경우에는 이 옵션이 적용되지 않습니다. 또한 별도의 중간자 없이 노드의 Port에서 파드의 Port로 바로 Forward 됩니다.

 

 

하지만 NodePort는 서비스이기 때문에 모든 노드의 해당 포트가 개방되게 되며 이는 실제로 파드가 없는 노드의 해당 포트도 개방된다는 의미입니다. 실제로 NodePort 서비스를 생성하면 쿠버네티스는 모든 워커 노드에 동일한 포트를 개방하며 해당 포트에 바인딩된 서비스가 요청을 파드로 흘려보내 주고, 실제로 특정 노드에 파드가 없다면 다른 노드의 파드로 연결을 Forward 해줍니다.

 

 

hostPort 기능은 주로 DaemonSet을 사용해서 Deploy된 시스템 서비스를 외부에 노출할 때 사용됩니다.

Using the node's PID and IPC namespaces

hostNetwork와 비슷하게 hostPID, hostIPC 값을 true로 설정할 수 있습니다. 이렇게 설정하게 되면 해당 파드가 노드의 PID와 IPC namespace를 사용하게 되므로 노드의 프로세스들과 IPC를 사용해서 통신할 수 있게 됩니다. 

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-host-pid-and-ipc
spec:
  hostPID: true
  hostIPC: true
  containers:
  - name: main
    image: alpine
    command: ["/bin/sleep", "999999"]

 

 

 

Configuring the container's Security Context

호스트 노드의 Namespace를 사용하는 것 이외에도 securityContext를 사용하면 추가적인 보안 설정들을 파드와 내부 컨테이너에 설정할 수 있습니다. securityContext는 다음 사항들을 설정할 수 있습니다.

 

  • 컨테이너 안의 프로세스가 어떤 User로 실행할지를 명시
  • 컨테이너가 Root 권한으로 실행되는 것을 막기
  • 컨테이너를 privileged mode로 실행 (노드 커널의 full access)
  • 프로세스가 컨테이너의 파일시스템에 write 하는 것을 막기
  • 권한의 세부 튜닝

 

Running a Pod without specifying a security context

아무런 SecurityContext를 설정하지 않고 파드를 실행해보도록 하겠습니다.

 

userID(uid), groupID(gid)가 모두 root로 실행되고 있는 것을 확인할 수 있습니다.

 

Running a Container as a Specific User

파드의 securityContext.runAsUser property를 설정하면 컨테이너를 guest user로 실행할 수 있습니다. 해당 property를 설정할 때는 username이 아닌 userId를 설정해주어야 합니다. (alpine image에서 guest user id는 405입니다.)

apiVersion: v1
kind: Pod
metadata:
  name: pod-as-user-guest
spec:
  containers:
  - name: main
    image: alpine
    command: ["/bin/sleep", "999999"]
    securityContext:
      runAsUser: 405        # guest

 

 

Preventing a container from running as root

파드의 컨테이너는 물론 호스트의 시스템과 격리되어 있지만 프로세스를 root 권한으로 실행되는 것은 권장되지 않습니다. 이를테면 컨테이너가 호스트의 디렉터리를 마운트 할 때 이를 root로 실행하게 되면 모든 권한을 갖게 되는 것을 의미하기 때문입니다. 악의적인 사용자가 image Registry에 대한 권한을 획득하고 해당 이미지에 악성 코드를 넣게 되면 이 컨테이너는 노드에 대한 모든 권한을 가지고 임의의 행동을 수행할 수 있으므로 이를 방지해야 합니다. securityContext.runAsNonRoot를 true로 설정하면 이를 방지할 수 있습니다.

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-run-as-non-root
spec:
  containers:
  - name: main
    image: alpine
    command: ["/bin/sleep", "999999"]
    securityContext:
      runAsNonRoot: true

 

 

Running Pods in privileged mode

어떤 경우에는 파드가 노드 커널에 접근하는 등 모든 권한을 부여받아야 할 때가 있습니다. 이를테면 kube-proxy 파드의 경우 프록시 서비스를 동작시키기 위해 노드의 iptables를 변경해야 합니다. 이러한 경우 securityContext.privileged의 값을 true로 설정하면 됩니다.

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-privileged
spec:
  containers:
  - name: main
    image: alpine
    command: ["/bin/sleep", "999999"]
    securityContext:
      privileged: true

 

Adding  & Dropping individual kernel capabilities to a container

Linux에서는 kernal capability로 권한을 관리하며, 쿠버네티스에서는 securityContext.capabilities 설정을 통해 파드에 노드 커널에 대한 특정 권한을 주거나, 특정 권한을 빼앗을 수 있습니다.

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-capability
spec:
  containers:
  - name: main
    image: alpine
    command: ["/bin/sleep", "999999"]
    securityContext:
      capabilities:
        add:            
        - SYS_TIME
        drop:
        - CHOWN

 

Preventing processes from writing to the container's filesystem

securityContext.readOnlyRootFilesystem을 true로 설정하면 해당 파드가 노드의 파일시스템을 readOnly로만 접근하도록 강제할 수 있습니다. 이렇게 하면 Root 권한을 가진 컨테이너더라도 파일 시스템에는 write 할 수 없으며 write 하기 위해서는 Volume을 Mount 해서 사용해야 합니다. 즉 기존 노드의 파일 시스템은 변경할 수 없게 됩니다.

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-readonly-filesystem
spec:
  containers:
  - name: main
    image: alpine
    command: ["/bin/sleep", "999999"]
    securityContext:
      readOnlyRootFilesystem: true      # write 는 불가능
    volumeMounts:
    - name: my-volume
      mountPath: /volume
      readOnly: false                   # volume 에는 write 가능
  volumes:
  - name: my-volume
    emptyDir:

 

Setting SeuciryContext Options At the Pod Level

securityContext 옵션은 컨테이너 레벨에서뿐 아니라 파드 레벨에서도 설정이 가능합니다. pod.spec.securityContext 프로퍼티를 이용해서 설정할 수 있으며 파드 레벨에서 설정하면 파드 안의 모든 컨테이너에 적용됩니다. 파드 레벨과 컨테이너 레벨에 모두 설정되어 있다면 파드 레벨의 securityContext를 컨테이너 레벨에서 override 하는 식으로 동작하게 됩니다.

 

 

Sharing volumes when containers run as different users

쿠버네티스 볼륨을 다룬 포스팅에서 하나의 파드 내의 서로 다른 컨테이너들이 볼륨을 사용하게 되면 데이터를 공유할 수 있다고 소개한 바 있습니다. 이것이 가능했던 이유는 서로 다른 컨테이너들이 모두 기본값인 root로 실행되어서 해당 볼륨에 대한 읽기 / 쓰기 권한을 모두 가지고 있었기 때문입니다. 만약 securityContext.runAsUser 옵션을 사용하게 되면 서로 다른 컨테이너들이 읽기 / 쓰기 권한이 없는 경우가 생기게 됩니다. 따라서 쿠버네티스에서는 supplemental groups를 제공하여 데이터 공유를 가능하게 도와줍니다. 파드 레벨에서 정의되는 fsGroup, supplementalGroups 옵션을 사용하면 됩니다. (하나의 파드 내의 서로 다른 컨테이너 간의 데이터 공유에 관한 옵션이므로 당연히 파드 레벨에서 정의해야 합니다.)

 

 

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-shared-volume-fsgroup
spec:
  securityContext:              
    fsGroup: 555
    supplementalGroups: [666, 777]
  containers:
  - name: first
    image: alpine
    command: ["/bin/sleep", "999999"]
    securityContext:
      runAsUser: 1111           
    volumeMounts:
    - name: shared-volume
      mountPath: /volume
      readOnly: false
  - name: second
    image: alpine
    command: ["/bin/sleep", "999999"]
    securityContext:
      runAsUser: 2222           
    volumeMounts:
    - name: shared-volume
      mountPath: /volume
      readOnly: false
  volumes:
  - name: shared-volume
    emptyDir:

 

fsGroup을 555로 설정하였으므로 마운트된 볼륨을 소유하고 있는 그룹아이디는 555입니다. 

 

실제로 해당 볼륨에 파일을 생성하면 파일을 소유하고 있는 아이디는 1111이고 그룹 아이디는 555가 됩니다.

 

 

 

Restricting the use of security-related features in pods

클러스터 관리자는 PodSecurityPolicy 리소스를 이용해서 파드의 보안과 관련된 기능들을 제한할 수 있습니다. 위의 섹션들에서 살펴본 securityContext는 파드와 파드 내의 컨테이너의 권한들을 제어하지만, 이를 생성하는 것은 사용자이기 때문에 파드를 deploy 하는 사용자는 사실상 클러스터 노드에 대한 모든 권한을 가지게 됩니다. 따라서 이를 제한할 방법이 필요하고 PodSecurityPolicy를 사용하면 이를 제한할 수 있습니다.

 

Introducing the PodSecurityPolicy resource

PodSecurityPolicy 리소스는 클러스터레벨의(non-namespaced)리소스입니다. 이는 클러스터 관리자가 생성하며 사용자(개발자)가 파드를 생성할 때 어떤 security와 관련된 옵션들을 사용할 수 있는지를 제한합니다. 사용자가 API 서버에 파드 리소스 정보를 담아 POST 요청을 보내면 API 서버 내의 PodSecurityPolicy admission control plugin에서 PodSecurityPolicy 정보를 가지고 해당 요청이 valid 한 지를 확인합니다. 유효한 요청이면 승인되어 리소스를 생성하고 유효하지 않은 요청인 경우 바로 reject 됩니다. 

 

Understanding what a PodSecurityPolicy can do

PodSecurityPolicy는 다음과 같은 역할을 수행할 수 있습니다. 위에서 살펴보았던 securityContext와 거의 동일합니다.

  • Pod 의 호스트 IPC / PID / 네트워크 namespace 제어
  • Pod 가 bind 할 수 있는 호스트의 포트 제한
  • 컨테이너를 실행할 user ID 제한
  • Privileged 컨테이너 실행 가능 여부
  • 커널 관련 작업 제어
  • 컨테이너의 root 파일 시스템 쓰기 제어
  • 컨테이너를 실행할 파일시스템 group 제한
  • Pod 가 사용할 수 있는 volume 종류 제한

PodSecurityPolicy는 다음과 같이 생성됩니다. 쿠버네티스 리소스이므로 yaml 파일로 쉽게 생성할 수 있습니다.

apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
  name: default
spec:
  hostIPC: false
  hostPID: false
  hostNetwork: false            
  hostPorts:
  - min: 10000
    max: 11000                  
  - min: 13000
    max: 14000                  
  privileged: false             
  readOnlyRootFilesystem: true  
  runAsUser:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  seLinux:
    rule: RunAsAny             
  volumes:
  - '*'

 

 

Understanding runAsUser, fsGroup and supplementalGroups policies

using the MustRunAs Rule

MustRunAs Rule을 사용하면 특정 ID, 혹은 특정 ID의 범위를 제한할 수 있습니다. 바로 위에서 RunAsAny를 사용했는데, 이를 사용하면 UserID나 GroupID에 대해 제한이 없으므로 권한 설정을 위해 범위를 제한하고 싶을 경우에는 MustRunAs Rule을 사용하면 됩니다.

runAsUser:
  rule: MustRunAs
  ranges:
  - min: 2
    max: 2
fsGroup:
  rule: MustRunAs
  ranges:
  - min: 2
    max: 10
  - min: 20
    max: 30
supplementalGroups:
  rule: MustRunAs
  ranges:
  - min: 2
    max: 10
  - min: 20
    max: 30

 

NOTE: PodSecurityPolicy 리소스를 업데이트하더라도 기존에 생성된 파드에 대해서는 영향을 주지 않습니다. 오로지 새롭게 생성된 파드나 기존 파드 중 리소스 업데이트 이후 변경된 파드에만 해당 사항이 적용됩니다.

 

Using the MustRunAsNonRoot Rule in the RunAsUser field

runAsUser필드에 MustRunAsNonRoot 옵션을 추가로 사용하게 되면 해당 설정이 적용된 파드는 root user로 실행할 수 없게 됩니다.

 

 

Configuring allowed, default, and disallowed capabilities

PodSecurityPolicy에서는 allowedCapabilities, defaultAddCapabilities, requiredDropCapabilities를 사용해 권한을 통제할 수 있습니다. 

apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
  name: default
spec:
  allowedCapabilities:
  - SYS_TIME
  defaultAddCapabilities:
  - CHOWN
  requiredDropCapabilities:
  - SYS_ADMIN
  - SYS_MODULE

 

allowedCapabilities는 위에서 securityContext.capabilities에서 사용자가 추가할 수 있는 권한들을 명시합니다. 즉 위의 예제에서는 SYS_TIME만 허용해주었으므로 해당 옵션이 적용된 클러스터에서 사용자가 파드를 생성할 때는 SYS_TIME만 추가할 수 있게 되는 것입니다. 즉, "추가 가능한" 옵션들을 알려주는 것이라고 할 수 있습니다.

 

defaultAddCapabilities는 기본적으로 파드에 추가되는 capabilities를 명시합니다. 즉, 사용자가 선택할 수 있는 것이 아닌 기본적으로 해당 클러스터 노드에서 파드를 생성하면 자동으로 생성되는 default capability를 의미하는 것입니다.

 

requiredDropCapabilities는 파드가 어떤 capability를 "가지지 않아야 하는지"를 명시하는 것이며 이를 사용하면 컨테이너가 해당 capability를 drop 하도록 강제합니다.

 

 

Constraining the types of volumes pods can use

PodSecurityPolicy가 여러개 있으면 각각의 Policy에서 허용한 볼륨 종류의 합집합이 사용 가능한 볼륨의 종류가 되며, 기본적으로 emptyDir, configMap, secret, downwardAPI, persistentVolumeClaim은 무조건 사용할 수 있도록 설정해주어야 합니다.

 

 

Assigning different PodSecurityPolicies to different users and groups

PodSecurityPolicy를 생성한 이후에는 어떤 사용자에게 어떤 Policy가 적용되어야 하는지를 할당할 수 있어야 합니다. PodSecurityPolicy는 사용자가 파드를 생성할 때 어떤 것들이 허용되고, 어떤 것들이 허용되지 않는지를 설정하는 것이기 때문에 사용자마다 다른 권한을 줄 수 있어야 하며 이는 ClusterRole과 ClusterRoleBinding을 사용해서 관리할 수 있습니다.

 

방법은 간단합니다. PodSecurityPolicy를 필요한 만큼 만들어 둔 뒤에 ClusterRole을 만들어서 PodSecurityPolicy를 참조하도록 설정하는 것입니다. 그 후 ClusterRoleBinding을 사용해서 특정 사용자나 그룹에게 ClusterRole을 바인딩하면 됩니다.

 

 

Isolating the pod network

쿠버네티스는 앞에서 살펴본 것처럼 파드와 컨테이너에 적용되는 보안 설정뿐 아니라, 파드 사이의 네트워크 통신에도 적용되는 보안 설정을 지원합니다. 기본적으로 네트워크 보안을 설정하기 위해서는 클러스터에서 사용하는 Network Plugin이 이를 지원해야 하며, 만약 지원하는 경우 NetworkPolicy 리소스를 생성하여 네트워크를 분리할 수 있습니다.

 

이 NetworkPolicy 리소스를 사용하면 ingress, egress 규칙을 설정할 수 있으므로 어떤 Source에서 트래픽을 받을지, 어떤 Destination으로 트래픽을 보낼지를 제한할 수 있습니다.

 

 

Enabling network isolation in a namespace

기본적으로 특정 namespace에 생성된 파드는 아무나 접근이 가능합니다. (can be accessed by anyone). 따라서 다음과 같이 NetworkPolicy 리소스를 생성하면 해당 namespace에 있는 어떤 파드들에도 접근할 수 없습니다. (podSelector를 빈칸으로 남겨두면 모든 파드에 적용되는 것을 의미합니다.)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector:

 

 

Allowing only some pods in the namespace to connect to a server pod

이제 기본적으로 아무도 파드에 접근할 수 없는 상태에서 특정 클라이언트와의 연결을 허용하려면 명시적으로 어떤 파드와 연결할 수 있는지를 알려주어야 합니다.

 

아래와 같이 설정하면 다음과 같은 규칙을 적용한다는 의미입니다.

  • app=database 레이블이 있는 파드에만 적용되는 규칙
  • app=webserver 레이블이 있는 파드로부터 들어오는 트래픽을 허용합니다.
  • 5432 포트를 통한 커넥션만 허용됩니다.

따라서 database 파드는 webserver파드로부터만 트래픽을 받도록 설정할 수 있고, 트래픽은 오직 5432포트를 통해서만 들어올 수 있게 됩니다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-netpolicy
spec:
  podSelector:
    matchLabels:
      app: database
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: webserver
    ports:
    - port: 5432

 

 

Isloating the network between Kubernetes namespaces

ingress에 namespaceSelector를 설정해주면 다양한 namespace로부터 오는 요청들을 허용할 수 있습니다. 아래와 같이 설정하면 tenant=manning 태그가 붙은 namespace로부터 오는 요청들은 80번 포트를 통해 허용할 수 있게 됩니다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: shoppingcart-netpolicy
spec:
  podSelector:
    matchLabels:
      app: shopping-cart
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          tenant: manning
    ports:
    - port: 80

 

Isolating using CIDR notation

labelSelector를 사용하는 대신 CIDR(Classless Inter Domain Routing)을 사용하여 제어할 수도 있습니다.

ingress:
- from:
  - ipBlock:
      cidr: 192.168.1.0/24    # 해당 block 의 트래픽만 허용

 

Limiting the outbound traffic of a set of pods

inbound Traffic이외에 밖으로 나가는 outbound Traffic도 동일하게 제어할 수 있습니다.

spec:
  podSelector:
    matchLabels:
      app: webserver
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - port: 5432

 

반응형