1 - AppArmor를 사용하여 리소스에 대한 컨테이너의 접근 제한

FEATURE STATE: Kubernetes v1.4 [beta]

AppArmor는 표준 리눅스 사용자와 그룹 기반의 권한을 보완하여, 한정된 리소스 집합으로 프로그램을 제한하는 리눅스 커널 보안 모듈이다. AppArmor는 임의의 애플리케이션에 대해서 잠재적인 공격 범위를 줄이고 더욱 심층적인 방어를 제공하도록 구성할 수 있다. 이 기능은 특정 프로그램이나 컨테이너에서 필요한 리눅스 기능, 네트워크 사용, 파일 권한 등에 대한 접근을 허용하는 프로파일로 구성한다. 각 프로파일은 허용하지 않은 리소스 접근을 차단하는 강제(enforcing) 모드 또는 위반만을 보고하는 불평(complain) 모드로 실행할 수 있다.

AppArmor를 이용하면 컨테이너가 수행할 수 있는 작업을 제한하고 또는 시스템 로그를 통해 더 나은 감사를 제공하여 더 안전한 배포를 실행할 수 있다. 그러나 AppArmor가 은탄환(언제나 통하는 무적의 방법)이 아니며, 애플리케이션 코드 취약점을 보호하기 위한 여러 조치를 할 수 있는 것 뿐임을 잊으면 안된다. 양호하고 제한적인 프로파일을 제공하고, 애플리케이션과 클러스터를 여러 측면에서 강화하는 것이 중요하다.

목적

  • 노드에 프로파일을 어떻게 적재하는지 예시를 본다.
  • 파드에 프로파일을 어떻게 강제 적용하는지 배운다.
  • 프로파일이 적재되었는지 확인하는 방법을 배운다.
  • 프로파일을 위반하는 경우를 살펴본다.
  • 프로파일을 적재할 수 없을 경우를 살펴본다.

시작하기 전에

다음을 보장해야 한다.

  1. 쿠버네티스 버전은 최소 1.4 이다. -- 쿠버네티스 v1.4부터 AppArmor 지원을 추가했다. v1.4 이전 쿠버네티스 컴포넌트는 새로운 AppArmor 어노테이션을 인식하지 못하고 제공되는 AppArmor 설정을 조용히 무시할 것이다. 파드에서 예상하는 보호를 받고 있는지 확인하려면 해당 노드의 Kubelet 버전을 확인하는 것이 중요하다.

    $ kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {@.status.nodeInfo.kubeletVersion}\n{end}'
    
    gke-test-default-pool-239f5d02-gyn2: v1.4.0
    gke-test-default-pool-239f5d02-x1kf: v1.4.0
    gke-test-default-pool-239f5d02-xwux: v1.4.0
    
  2. AppArmor 커널 모듈을 사용 가능해야 한다. -- 리눅스 커널에 AppArmor 프로파일을 강제 적용하기 위해 AppArmor 커널 모듈은 반드시 설치되어 있고 사용 가능해야 한다. 예를 들어 Ubuntu 및 SUSE 같은 배포판은 모듈을 기본값으로 지원하고, 그 외 많은 다른 배포판들은 선택적으로 지원한다. 모듈이 사용 가능한지 확인하려면 /sys/module/apparmor/parameters/enabled 파일을 확인한다.

    $ cat /sys/module/apparmor/parameters/enabled
    Y
    

    Kubelet(>=v1.4)이 AppArmor 기능 지원을 포함하지만, 커널 모듈을 사용할 수 없으면 파드에서 AppArmor 옵션을 실행하는 것이 거부된다.

  1. 컨테이너 런타임이 AppArmor을 지원한다. -- 현재 모든 일반적인 쿠버네티스를 지원하는 도커(Docker), CRI-O 또는 containerd 와 같은 컨테이너 런타임들은 AppArmor를 지원해야 한다. 이 런타임 설명서를 참조해서 클러스터가 AppArmor를 사용하기 위한 요구 사항을 충족하는지 확인해야 한다.

  2. 프로파일이 적재되어 있다. -- AppArmor는 각 컨테이너와 함께 실행해야 하는 AppArmor 프로파일을 지정하여 파드에 적용한다. 커널에 지정한 프로파일이 적재되지 않았다면, Kubelet(>= v1.4)은 파드를 거부한다. 해당 노드에 어떤 프로파일이 적재되었는지는 /sys/kernel/security/apparmor/profiles 파일을 통해 확인할 수 있다. 예를 들어,

    $ ssh gke-test-default-pool-239f5d02-gyn2 "sudo cat /sys/kernel/security/apparmor/profiles | sort"
    
    apparmor-test-deny-write (enforce)
    apparmor-test-audit-write (enforce)
    docker-default (enforce)
    k8s-nginx (enforce)
    

    노드에 프로파일을 적재하는 것에 대해 더 자세한 내용은 프로파일과 함께 노드 설정하기.

AppArmor 지원이 포함된 Kubelet (>= v1.4)이면 어떤 전제 조건이 충족되지 않으면 AppArmor와 함께한 파드를 거부한다. 노드 상에 AppArmor 지원 여부는 노드 준비 조건 메시지를 확인하여(이후 릴리스에서는 삭제될 것 같지만) 검증할 수 있다.

kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {.status.conditions[?(@.reason=="KubeletReady")].message}\n{end}'
gke-test-default-pool-239f5d02-gyn2: kubelet is posting ready status. AppArmor enabled
gke-test-default-pool-239f5d02-x1kf: kubelet is posting ready status. AppArmor enabled
gke-test-default-pool-239f5d02-xwux: kubelet is posting ready status. AppArmor enabled

파드 보안 강화하기

AppArmor 프로파일은 컨테이너마다 지정된다. 함께 실행할 파드 컨테이너에 AppArmor 프로파일을 지정하려면 파드의 메타데이터에 어노테이션을 추가한다.

container.apparmor.security.beta.kubernetes.io/<container_name>: <profile_ref>

<container_name>은 프로파일을 적용하는 컨테이너 이름이고, <profile_ref>는 적용할 프로파일을 지정한다. profile_ref는 다음 중에 하나이다.

  • 런타임의 기본 프로파일을 적용하기 위한 runtime/default
  • <profile_name>로 이름한 호스트에 적재되는 프로파일을 적용하기 위한 localhost/<profile_name>
  • 적재할 프로파일이 없음을 나타내는 unconfined

어노테이션과 프로파일 이름 형식의 자세한 내용은 API 참조를 살펴본다.

쿠버네티스 AppArmor 의 작동 순서는 모든 선행 조건이 충족되었는지 확인하고, 적용을 위해 선택한 프로파일을 컨테이너 런타임으로 전달하여 이루어진다. 만약 선행 조건이 충족되지 않으면 파드는 거부되고 실행되지 않는다.

프로파일이 적용되었는지 확인하기 위해, 컨테이너 생성 이벤트에 나열된 AppArmor 보안 옵션을 찾아 볼 수 있다.

kubectl get events | grep Created
22s        22s         1         hello-apparmor     Pod       spec.containers{hello}   Normal    Created     {kubelet e2e-test-stclair-node-pool-31nt}   Created container with docker id 269a53b202d3; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]apparmor=k8s-apparmor-example-deny-write]

컨테이너의 루트 프로세스가 올바른 프로파일로 실행되는지는 proc attr을 확인하여 직접 검증할 수 있다.

kubectl exec <pod_name> cat /proc/1/attr/current
k8s-apparmor-example-deny-write (enforce)

예시

이 예시는 AppArmor를 지원하는 클러스터를 이미 구성하였다고 가정한다.

먼저 노드에서 사용하려는 프로파일을 적재해야 한다. 사용할 프로파일은 파일 쓰기를 거부한다.

#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # Deny all file writes.
  deny /** w,
}

파드를 언제 스케줄할지 알지 못하므로 모든 노드에 프로파일을 적재해야 한다. 이 예시에서는 SSH를 이용하여 프로파일을 설치할 것이나 다른 방법은 프로파일과 함께 노드 설정하기에서 논의한다.

NODES=(
    # The SSH-accessible domain names of your nodes
    gke-test-default-pool-239f5d02-gyn2.us-central1-a.my-k8s
    gke-test-default-pool-239f5d02-x1kf.us-central1-a.my-k8s
    gke-test-default-pool-239f5d02-xwux.us-central1-a.my-k8s)
for NODE in ${NODES[*]}; do ssh $NODE 'sudo apparmor_parser -q <<EOF
#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # Deny all file writes.
  deny /** w,
}
EOF'
done

다음으로 쓰기 금지 프로파일된 "Hello AppArmor" 파드를 실행한다.

apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor
  annotations:
    # 쿠버네티스에 'k8s-apparmor-example-deny-write' AppArmor 프로파일을 적용함을 알린다.
    # 잊지 말 것은 쿠버네티스 노드에서 실행 중인 버전이 1.4 이상이 아닌 경우에는 이 설정은 무시된다는 것이다.
    container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-deny-write
spec:
  containers:
  - name: hello
    image: busybox
    command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
kubectl create -f ./hello-apparmor.yaml

파드 이벤트를 살펴보면, 'k8s-apparmor-example-deny-write' AppArmor 프로파일로 생성된 파드 컨테이너를 확인할 수 있다.

kubectl get events | grep hello-apparmor
14s        14s         1         hello-apparmor   Pod                                Normal    Scheduled   {default-scheduler }                           Successfully assigned hello-apparmor to gke-test-default-pool-239f5d02-gyn2
14s        14s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Pulling     {kubelet gke-test-default-pool-239f5d02-gyn2}   pulling image "busybox"
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Pulled      {kubelet gke-test-default-pool-239f5d02-gyn2}   Successfully pulled image "busybox"
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Created     {kubelet gke-test-default-pool-239f5d02-gyn2}   Created container with docker id 06b6cd1c0989; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Started     {kubelet gke-test-default-pool-239f5d02-gyn2}   Started container with docker id 06b6cd1c0989

proc attr을 확인하여 컨테이너가 실제로 해당 프로파일로 실행 중인지 확인할 수 있다.

kubectl exec hello-apparmor -- cat /proc/1/attr/current
k8s-apparmor-example-deny-write (enforce)

마지막으로 파일 쓰기를 통해 프로파일을 위반하면 어떻게 되는지 확인할 수 있다.

kubectl exec hello-apparmor -- touch /tmp/test
touch: /tmp/test: Permission denied
error: error executing remote command: command terminated with non-zero exit code: Error executing in Docker Container: 1

이제 정리하면서, 적재되지 않은 프로파일을 지정하면 어떻게 되는지 살펴본다.

kubectl create -f /dev/stdin <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor-2
  annotations:
    container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-allow-write
spec:
  containers:
  - name: hello
    image: busybox
    command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
EOF
pod/hello-apparmor-2 created
kubectl describe pod hello-apparmor-2
Name:          hello-apparmor-2
Namespace:     default
Node:          gke-test-default-pool-239f5d02-x1kf/
Start Time:    Tue, 30 Aug 2016 17:58:56 -0700
Labels:        <none>
Annotations:   container.apparmor.security.beta.kubernetes.io/hello=localhost/k8s-apparmor-example-allow-write
Status:        Pending
Reason:        AppArmor
Message:       Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded
IP:
Controllers:   <none>
Containers:
  hello:
    Container ID:
    Image:     busybox
    Image ID:
    Port:
    Command:
      sh
      -c
      echo 'Hello AppArmor!' && sleep 1h
    State:              Waiting
      Reason:           Blocked
    Ready:              False
    Restart Count:      0
    Environment:        <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-dnz7v (ro)
Conditions:
  Type          Status
  Initialized   True
  Ready         False
  PodScheduled  True
Volumes:
  default-token-dnz7v:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-dnz7v
    Optional:   false
QoS Class:      BestEffort
Node-Selectors: <none>
Tolerations:    <none>
Events:
  FirstSeen    LastSeen    Count    From                        SubobjectPath    Type        Reason        Message
  ---------    --------    -----    ----                        -------------    --------    ------        -------
  23s          23s         1        {default-scheduler }                         Normal      Scheduled     Successfully assigned hello-apparmor-2 to e2e-test-stclair-minion-group-t1f5
  23s          23s         1        {kubelet e2e-test-stclair-node-pool-t1f5}             Warning        AppArmor    Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded

파드 상태는 Pending이며, 오류 메시지는 Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded이다. 이벤트도 동일한 메시지로 기록되었다.

관리

프로파일과 함께 노드 설정하기

현재 쿠버네티스는 AppArmor 프로파일을 노드에 적재하기 위한 네이티브 메커니즘을 제공하지 않는다. 프로파일을 설정하는 여러 방법이 있다. 예를 들면 다음과 같다.

  • 각 노드에서 파드를 실행하는 데몬셋을 통해서 올바른 프로파일이 적재되었는지 확인한다. 예시 구현은 여기에서 찾아볼 수 있다.
  • 노드 초기화 시간에 노드 초기화 스크립트(예를 들어 Salt, Ansible 등)나 이미지를 이용
  • 예시에서 보여준 것처럼, 프로파일을 각 노드에 복사하고 SSH를 통해 적재한다.

스케줄러는 어떤 프로파일이 어떤 노드에 적재되는지 고려하지 않으니, 프로파일 전체 집합이 모든 노드에 적재되어야 한다. 대안적인 방법은 각 프로파일(혹은 프로파일의 클래스)을 위한 노드 레이블을 노드에 추가하고, 노드 셀렉터를 이용하여 파드가 필요한 프로파일이 있는 노드에서 실행되도록 한다.

파드시큐리티폴리시(PodSecurityPolicy)로 프로파일 제한하기

만약 파드시큐리티폴리시 확장을 사용하면, 클러스터 단위로 AppArmor 제한을 적용할 수 있다. 파드시큐리티폴리시를 사용하려면 위해 다음의 플래그를 반드시 apiserver에 설정해야 한다.

--enable-admission-plugins=PodSecurityPolicy[,others...]

AppArmor 옵션은 파드시큐리티폴리시의 어노테이션으로 지정할 수 있다.

apparmor.security.beta.kubernetes.io/defaultProfileName: <profile_ref>
apparmor.security.beta.kubernetes.io/allowedProfileNames: <profile_ref>[,others...]

기본 프로파일 이름 옵션은 프로파일을 지정하지 않았을 때에 컨테이너에 기본으로 적용하는 프로파일을 지정한다. 허용하는 프로파일 이름 옵션은 파드 컨테이너가 함께 실행하도록 허용되는 프로파일 목록을 지정한다. 두 옵션을 모두 사용하는 경우, 기본값은 반드시 필요하다. 프로파일은 컨테이너에서 같은 형식으로 지정된다. 전체 사양은 API 참조를 찾아본다.

AppArmor 비활성화

클러스터에서 AppArmor 사용하지 않으려면, 커맨드라인 플래그로 비활성화 할 수 있다.

--feature-gates=AppArmor=false

비활성화되면 AppArmor 프로파일을 포함한 파드는 "Forbidden" 오류로 검증 실패한다. 기본적으로 도커는 항상 비특권 파드에 "docker-default" 프로파일을 활성화하고(AppArmor 커널모듈이 활성화되었다면), 기능 게이트가 비활성화된 것처럼 진행한다. AppArmor를 비활성화하는 이 옵션은 AppArmor가 일반 사용자 버전이 되면 제거된다.

AppArmor와 함께 쿠버네티스 1.4로 업그레이드 하기

클러스터 버전을 v1.4로 업그레이드하기 위해 AppArmor쪽 작업은 없다. 그러나 AppArmor 어노테이션을 가진 파드는 유효성 검사(혹은 파드시큐리티폴리시 승인)을 거치지 않는다. 그 노드에 허용 프로파일이 로드되면, 악의적인 사용자가 허가 프로필을 미리 적용하여 파드의 권한을 docker-default 보다 높일 수 있다. 이것이 염려된다면 apparmor.security.beta.kubernetes.io 어노테이션이 포함된 모든 파드의 클러스터를 제거하는 것이 좋다.

일반 사용자 버전으로 업그레이드 방법

AppArmor는 일반 사용자 버전(general available)으로 준비되면 현재 어노테이션으로 지정되는 옵션은 필드로 변경될 것이다. 모든 업그레이드와 다운그레이드 방법은 전환을 통해 지원하기에는 매우 미묘하니 전환이 필요할 때에 상세히 설명할 것이다. 최소 두 번의 릴리스에 대해서는 필드와 어노테이션 모두를 지원할 것이고, 그 이후부터는 어노테이션은 명확히 거부된다.

프로파일 제작

AppArmor 프로파일을 만들고 올바르게 지정하는 것은 매우 까다로울 수 있다. 다행히 이 작업에 도움 되는 도구가 있다.

  • aa-genprofaa-logprof는 애플리케이션 활동과 로그와 수행에 필요한 행동을 모니터링하여 일반 프로파일 규칙을 생성한다. 자세한 사용방법은 AppArmor 문서에서 제공한다.
  • bane은 단순화된 프로파일 언어를 이용하는 도커를 위한 AppArmor 프로파일 생성기이다.

개발 장비의 도커를 통해 애플리케이션을 실행하여 프로파일을 생성하는 것을 권장하지만, 파드가 실행 중인 쿠버네티스 노드에서 도구 실행을 금하지는 않는다.

AppArmor 문제를 디버깅하기 위해서 거부된 것으로 보이는 시스템 로그를 확인할 수 있다. AppArmor 로그는 dmesg에서 보이며, 오류는 보통 시스템 로그나 journalctl에서 볼 수 있다. 더 많은 정보는 AppArmor 실패에서 제공한다.

API 참조

파드 어노테이션

컨테이너를 실행할 프로파일을 지정한다.

  • : container.apparmor.security.beta.kubernetes.io/<container_name> <container_name>는 파드 내에 컨테이너 이름과 일치한다. 분리된 프로파일은 파드 내에 각 컨테이너로 지정할 수 있다.
  • : 아래 기술된 프로파일 참조

프로파일 참조

  • runtime/default: 기본 런타임 프로파일을 참조한다.
    • (기본 파드시큐리티폴리시 없이) 프로파일을 지정하지 않고 AppArmor를 사용하는 것과 동등하다.
    • 도커에서는 권한 없는 컨테이너의 경우는 docker-default 프로파일로, 권한이 있는 컨테이너의 경우 unconfined(프로파일 없음)으로 해석한다.
  • localhost/<profile_name>: 노드(localhost)에 적재된 프로파일을 이름으로 참조한다.
  • unconfined: 이것은 컨테이너에서 AppArmor를 효과적으로 비활성시킨다.

다른 어떤 프로파일 참조 형식도 유효하지 않다.

파드시큐리티폴리시 어노테이션

아무 프로파일도 제공하지 않을 때에 컨테이너에 적용할 기본 프로파일을 지정하기

  • : apparmor.security.beta.kubernetes.io/defaultProfileName
  • : 프로파일 참조. 위에 기술됨.

파드 컨테이너에서 지정을 허용하는 프로파일 목록 지정하기

  • : apparmor.security.beta.kubernetes.io/allowedProfileNames
  • : 컴마로 구분된 참조 프로파일 목록(위에 기술함)
    • 비록 이스케이프된 쉼표(%2C ',')도 프로파일 이름에서 유효한 문자이지만 여기에서 명시적으로 허용하지 않는다.

다음 내용

참고 자료

2 - 파드 시큐리티 스탠다드를 네임스페이스 수준에 적용하기

파드 시큐리티 어드미션(PSA, Pod Security Admission)은 베타로 변경되어 v1.23 이상에서 기본적으로 활성화되어 있다. 파드 시큐리티 어드미션은 파드가 생성될 때 파드 시큐리티 스탠다드(Pod Security Standards)를 적용하는 어드미션 컨트롤러이다. 이 튜토리얼에서는, 각 네임스페이스별로 baseline 파드 시큐리티 스탠다드를 강제(enforce)할 것이다.

파드 시큐리티 스탠다드를 클러스터 수준에서 여러 네임스페이스에 한 번에 적용할 수도 있다. 이에 대한 안내는 파드 시큐리티 스탠다드를 클러스터 수준에 적용하기를 참고한다.

시작하기 전에

워크스테이션에 다음을 설치한다.

클러스터 생성하기

  1. 다음과 같이 KinD 클러스터를 생성한다.

    kind create cluster --name psa-ns-level --image kindest/node:v1.23.0
    

    다음과 비슷하게 출력될 것이다.

    Creating cluster "psa-ns-level" ...
     ✓ Ensuring node image (kindest/node:v1.23.0) 🖼 
     ✓ Preparing nodes 📦  
     ✓ Writing configuration 📜 
     ✓ Starting control-plane 🕹️ 
     ✓ Installing CNI 🔌 
     ✓ Installing StorageClass 💾 
    Set kubectl context to "kind-psa-ns-level"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-ns-level
    
    Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
    
  2. kubectl context를 새로 생성한 클러스터로 설정한다.

    kubectl cluster-info --context kind-psa-ns-level
    

    다음과 비슷하게 출력될 것이다.

    Kubernetes control plane is running at https://127.0.0.1:50996
    CoreDNS is running at https://127.0.0.1:50996/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    

네임스페이스 생성하기

example이라는 네임스페이스를 생성한다.

kubectl create ns example

다음과 비슷하게 출력될 것이다.

namespace/example created

파드 시큐리티 스탠다드 적용하기

  1. 내장 파드 시큐리티 어드미션이 지원하는 레이블을 사용하여 이 네임스페이스에 파드 시큐리티 스탠다드를 활성화한다. 이 단계에서는 latest 버전(기본값)에 따라 baseline(기준) 파드 시큐리티 스탠다드에 대해 경고를 설정한다.

    kubectl label --overwrite ns example \
      pod-security.kubernetes.io/warn=baseline \
      pod-security.kubernetes.io/warn-version=latest
    
  2. 어떠한 네임스페이스에도 복수 개의 파드 시큐리티 스탠다드를 활성화할 수 있으며, 이는 레이블을 이용하여 가능하다. 다음 명령어는 최신 버전(기본값)에 따라, baseline(기준) 파드 시큐리티 스탠다드는 enforce(강제)하지만 restricted(제한된) 파드 시큐리티 스탠다드에 대해서는 warn(경고)audit(감사)하도록 설정한다.

    kubectl label --overwrite ns example \
      pod-security.kubernetes.io/enforce=baseline \
      pod-security.kubernetes.io/enforce-version=latest \
      pod-security.kubernetes.io/warn=restricted \
      pod-security.kubernetes.io/warn-version=latest \
      pod-security.kubernetes.io/audit=restricted \
      pod-security.kubernetes.io/audit-version=latest
    

파드 시큐리티 스탠다드 검증하기

  1. example 네임스페이스에 최소한의 파드를 생성한다.

    cat <<EOF > /tmp/pss/nginx-pod.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80
    EOF
    
  2. 클러스터의 example 네임스페이스에 해당 파드 스펙을 적용한다.

    kubectl apply -n example -f /tmp/pss/nginx-pod.yaml
    

    다음과 비슷하게 출력될 것이다.

    Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
    pod/nginx created
    
  3. 클러스터의 default 네임스페이스에 해당 파드 스펙을 적용한다.

     kubectl apply -n default -f /tmp/pss/nginx-pod.yaml
    

    다음과 비슷하게 출력될 것이다.

    pod/nginx created
    

파드 시큐리티 스탠다드는 example 네임스페이스에만 적용되었다. 동일한 파드를 default 네임스페이스에 생성하더라도 경고가 발생하지 않는다.

정리하기

kind delete cluster -name psa-ns-level 명령을 실행하여 생성했던 클러스터를 삭제한다.

다음 내용

3 - 파드 시큐리티 스탠다드를 클러스터 수준에 적용하기

파드 시큐리티 어드미션(PSA, Pod Security Admission)은 베타로 변경되어 v1.23 이상에서 기본적으로 활성화되어 있다. 파드 시큐리티 어드미션은 파드가 생성될 때 파드 시큐리티 스탠다드(Pod Security Standards)를 적용하는 어드미션 컨트롤러이다. 이 튜토리얼은 baseline 파드 시큐리티 스탠다드를 클러스터 수준(level)에 적용하여 표준 구성을 클러스터의 모든 네임스페이스에 적용하는 방법을 보여 준다.

파드 시큐리티 스탠다드를 특정 네임스페이스에 적용하려면, 파드 시큐리티 스탠다드를 네임스페이스 수준에 적용하기를 참고한다.

시작하기 전에

워크스테이션에 다음을 설치한다.

적용할 알맞은 파드 시큐리티 스탠다드 선택하기

파드 시큐리티 어드미션을 이용하여 enforce, audit, 또는 warn 모드 중 하나로 내장 파드 시큐리티 스탠다드를 적용할 수 있다.

현재 구성에 가장 적합한 파드 시큐리티 스탠다드를 고르는 데 도움이 되는 정보를 수집하려면, 다음을 수행한다.

  1. 파드 시큐리티 스탠다드가 적용되지 않은 클러스터를 생성한다.

    kind create cluster --name psa-wo-cluster-pss --image kindest/node:v1.23.0
    

    다음과 비슷하게 출력될 것이다.

    Creating cluster "psa-wo-cluster-pss" ...
    ✓ Ensuring node image (kindest/node:v1.23.0) 🖼
    ✓ Preparing nodes 📦  
    ✓ Writing configuration 📜
    ✓ Starting control-plane 🕹️
    ✓ Installing CNI 🔌
    ✓ Installing StorageClass 💾
    Set kubectl context to "kind-psa-wo-cluster-pss"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-wo-cluster-pss
    
    Thanks for using kind! 😊
    
    
  2. kubectl context를 새로 생성한 클러스터로 설정한다.

    kubectl cluster-info --context kind-psa-wo-cluster-pss
    

    다음과 비슷하게 출력될 것이다.

     Kubernetes control plane is running at https://127.0.0.1:61350
    
    CoreDNS is running at https://127.0.0.1:61350/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  3. 클러스터의 네임스페이스 목록을 조회한다.

    kubectl get ns
    

    다음과 비슷하게 출력될 것이다.

    NAME                 STATUS   AGE
    default              Active   9m30s
    kube-node-lease      Active   9m32s
    kube-public          Active   9m32s
    kube-system          Active   9m32s
    local-path-storage   Active   9m26s
    
  4. --dry-run=server를 사용하여 다른 파드 시큐리티 스탠다드가 적용되었을 때 어떤 것이 변경되는지 확인한다.

    1. Privileged
      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=privileged
      

    다음과 비슷하게 출력될 것이다.

    namespace/default labeled
    namespace/kube-node-lease labeled
    namespace/kube-public labeled
    namespace/kube-system labeled
    namespace/local-path-storage labeled
    
    1. Baseline
      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=baseline
      

    다음과 비슷하게 출력될 것이다.

    namespace/default labeled
    namespace/kube-node-lease labeled
    namespace/kube-public labeled
    Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "baseline:latest"
    Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes
    Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes
    Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged
    namespace/kube-system labeled
    namespace/local-path-storage labeled
    
    1. Restricted
     kubectl label --dry-run=server --overwrite ns --all \
     pod-security.kubernetes.io/enforce=restricted
    

    다음과 비슷하게 출력될 것이다.

    namespace/default labeled
    namespace/kube-node-lease labeled
    namespace/kube-public labeled
    Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "restricted:latest"
    Warning: coredns-7bb9c7b568-hsptc (and 1 other pod): unrestricted capabilities, runAsNonRoot != true, seccompProfile
    Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true
    Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
    Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
    namespace/kube-system labeled
    Warning: existing pods in namespace "local-path-storage" violate the new PodSecurity enforce level "restricted:latest"
    Warning: local-path-provisioner-d6d9f7ffc-lw9lh: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile
    namespace/local-path-storage labeled
    

위의 출력에서, privileged 파드 시큐리티 스탠다드를 적용하면 모든 네임스페이스에서 경고가 발생하지 않는 것을 볼 수 있다. 그러나 baselinerestricted 파드 시큐리티 스탠다드에 대해서는 kube-system 네임스페이스에서 경고가 발생한다.

모드, 버전, 및 파드 시큐리티 스탠다드 설정

이 섹션에서는, 다음의 파드 시큐리티 스탠다드를 latest 버전에 적용한다.

  • baseline 파드 시큐리티 스탠다드는 enforce 모드로 적용
  • restricted 파드 시큐리티 스탠다드는 warnaudit 모드로 적용

baseline 파드 시큐리티 스탠다드는 예외 목록을 간결하게 유지하고 알려진 권한 상승(privilege escalations)을 방지할 수 있는 편리한 절충안을 제공한다.

추가적으로, kube-system 내의 파드가 실패하는 것을 방지하기 위해, 해당 네임스페이스는 파드 시큐리티 스탠다드가 적용되지 않도록 제외할 것이다.

사용 중인 환경에 파드 시큐리티 어드미션을 적용할 때에는 다음의 사항을 고려한다.

  1. 클러스터에 적용된 위험 상태에 따라, restricted와 같은 더 엄격한 파드 시큐리티 스탠다드가 더 좋을 수도 있다.

  2. kube-system 네임스페이스를 적용 대상에서 제외하면 이 네임스페이스의 파드가 privileged로 실행될 수 있다. 실제 사용 환경에서는, 최소 권한 원칙을 준수하도록, 접근을 kube-system 네임스페이스로 제한하는 엄격한 RBAC 정책을 적용할 것을 강력히 권장한다.

  3. 파드 시큐리티 어드미션 컨트롤러가 이러한 파드 시큐리티 스탠다드를 구현하는 데 사용할 수 있는 구성 파일을 생성한다.

    mkdir -p /tmp/pss
    cat <<EOF > /tmp/pss/cluster-level-pss.yaml 
    apiVersion: apiserver.config.k8s.io/v1
    kind: AdmissionConfiguration
    plugins:
    - name: PodSecurity
      configuration:
        apiVersion: pod-security.admission.config.k8s.io/v1beta1
        kind: PodSecurityConfiguration
        defaults:
          enforce: "baseline"
          enforce-version: "latest"
          audit: "restricted"
          audit-version: "latest"
          warn: "restricted"
          warn-version: "latest"
        exemptions:
          usernames: []
          runtimeClasses: []
          namespaces: [kube-system]
    EOF
    
  4. API 서버가 클러스터 생성 과정에서 이 파일을 처리할 수 있도록 구성한다.

    cat <<EOF > /tmp/pss/cluster-config.yaml 
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    nodes:
    - role: control-plane
      kubeadmConfigPatches:
      - |
        kind: ClusterConfiguration
        apiServer:
            extraArgs:
              admission-control-config-file: /etc/config/cluster-level-pss.yaml
            extraVolumes:
              - name: accf
                hostPath: /etc/config
                mountPath: /etc/config
                readOnly: false
                pathType: "DirectoryOrCreate"
      extraMounts:
      - hostPath: /tmp/pss
        containerPath: /etc/config
        # optional: if set, the mount is read-only.
        # default false
        readOnly: false
        # optional: if set, the mount needs SELinux relabeling.
        # default false
        selinuxRelabel: false
        # optional: set propagation mode (None, HostToContainer or Bidirectional)
        # see https://kubernetes.io/docs/concepts/storage/volumes/#mount-propagation
        # default None
        propagation: None
    EOF
    
  5. 이러한 파드 시큐리티 스탠다드를 적용하기 위해 파드 시큐리티 어드미션을 사용하는 클러스터를 생성한다.

     kind create cluster --name psa-with-cluster-pss --image kindest/node:v1.23.0 --config /tmp/pss/cluster-config.yaml
    

    다음과 비슷하게 출력될 것이다.

     Creating cluster "psa-with-cluster-pss" ...
      ✓ Ensuring node image (kindest/node:v1.23.0) 🖼 
      ✓ Preparing nodes 📦  
      ✓ Writing configuration 📜 
      ✓ Starting control-plane 🕹️ 
      ✓ Installing CNI 🔌 
      ✓ Installing StorageClass 💾 
     Set kubectl context to "kind-psa-with-cluster-pss"
     You can now use your cluster with:
    
     kubectl cluster-info --context kind-psa-with-cluster-pss
    
     Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂
    
  6. kubectl context를 새로 생성한 클러스터로 설정한다.

     kubectl cluster-info --context kind-psa-with-cluster-pss
    

    다음과 비슷하게 출력될 것이다.

     Kubernetes control plane is running at https://127.0.0.1:63855
     CoreDNS is running at https://127.0.0.1:63855/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
     To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  7. 기본 네임스페이스에 생성할 최소한의 구성에 대한 파드 명세를 다음과 같이 생성한다.

    cat <<EOF > /tmp/pss/nginx-pod.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80
    EOF
    
  8. 클러스터에 해당 파드를 생성한다.

     kubectl apply -f /tmp/pss/nginx-pod.yaml
    

    다음과 비슷하게 출력될 것이다.

     Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
     pod/nginx created
    

정리하기

kind delete cluster --name psa-with-cluster-psskind delete cluster --name psa-wo-cluster-pss 명령을 실행하여 생성했던 클러스터를 삭제한다.

다음 내용