티스토리 뷰
<출처>
https://kubernetes.io/docs/tutorials/clusters/apparmor/
FEATURE STATE: Kubernetes v1.4
beta
AppArmor는 리눅스 커널 보안 모듈로 제한된 리소스 셋에 대해 프로그램을 한정시키기 위해 퍼미션을 기반으로 표준 리눅스 사용자와 그룹 정책을 보완해준다. AppArmor는 잠재적인 공격영역을 축소시키고 단단한 철벽 방어력을 제공할 수 있도록 어떤 임의의 애플리케이션에 대해서도 구성이 가능하다. 리눅스 처리용량, 네트워크 접근, 파일 퍼미션 등과 같이 특정 프로그램 또는 컨테이너에 의해 요구되어지는 접근은 화이트리스트에서 조율한 프로파일을 통해 구성된다. 각 프로파일은 허용하지 않은 리소스에 대한 접근을 차단시키는 "enforcing" 모드 또는 오직 위반에 대한 보고만 하는 "complain" 모드로 동작될 수 있다.
AppArmor는 컨테이너가 무엇을 하도록 허용할 것인지에 대한 제한을 통해 보다 안전한 디플로이먼트를 동작시킬 수 있도록 도와주고 시스템 로그를 통해 보다 개선된 감사기능을 제공할 수 있도록 도와준다, 그러나, AppArmor가 묘책이 아니라는 것을 명심하는 것이 중요하다. 그저 애플리케이션 코드 내에서의 부당한 이용가능성을 방지해주는 데 도움이 될 수 있을 뿐이다. 적절한, 제한적인 프로파일을 제공하고 다양한 각도에서 애플리케이션과 클러스터를 강화해 나는 것이 중요하다.
Objectives
- 노드에 프로파일을 로드하는 방법에 대한 예시를 알아본다.
- 파드에 프로파일을 강화시키는 방법에 대해 배운다.
- 로드된 프로파일을 체크하는 방법에 대해 배운다.
- 프로파일이 위배된 경우 무슨일이 일어나는지 살펴본다.
- 프로파일이 로드될 수 없는 경우 무슨 일이 일어나는지 살펴본다.
Before you begin
다음 사항을 확인한다.
1. K8S 버전은 최소 1.4여야 한다. – K8S는 버전 1.4에서 AppArmor 지원기능이 추가되었다. 1.4 이전 버전의 K8S 컴포넌트는 새로운 AppArmor 주석(annotation)을 인지하지 못하며, 제공된 AppArmor 설정들은 아무런 반응 없이 무시될 것이다. 파드가 예상한 대로 보호 받고 있는지 알아보기 위해, 노드에 대한 Kubelet 버전을 확인하는 것이 중요하다.
1 2 | root@zerobig-vm-u:~# kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {@.status.nodeInfo.kubeletVersion}\n{end}' minikube: v1.10.0 | cs |
/sys/module/apparmor/parameters/enabled
파일을 확인한다.1 2 | root@zerobig-vm-u:~# cat /sys/module/apparmor/parameters/enabled Y | cs |
Kubelet이 AppArmor를 지원을(>= v1.4) 포함하는 경우, 커널 모듈이 사용하지 않도록 설정된 AppArmor 옵션에서는 파드 동작이 거부될 것이다,
주의: 우분투는 부가적인 hook 과 특징을 추가하는 패치를 포함하여 업스트림 리눅스 커널로 병합된 많은 AppArmor 패치들을 가진다. K8S는 오직 업스트림 버전을 이용해 테스트가 수행 되었으므로 다른 특징들에 대한 지원에 대한 보장을 하지 않는다.
3. 컨테이너 런타임은 Docker이다. – 현재 AppArmor를 지원하는 유일한 K8S 지원 컨테이너 런타임은 Docker이다. 더 많은 런타임들이 AppArmor 지원을 추가하고 있으므로, 옵션은 확장되어질 것이다, 현재 노드가 Docker와 동작 중인지 확인해 본다.
1 2 | root@zerobig-vm-u:~# kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {@.status.nodeInfo.containerRuntimeVersion}\n{end}' minikube: docker://17.12.1-ce | cs |
Kubelet이 AppArmor를 지원을(>= v1.4) 포함하는 경우, 런타임이 Docker가 아닌 AppArmor 옵션에서는 파드 동작이 거부될 것이다,
4. Profile이 로드 되었다. – AppArmor는 AppArmor 프로파일을 정의하여 파드에 적용된다. 각각의 컨테이너는 이 프로파일과 함께 동작되어야 한다. 정의한 프로파일 중 어떠한 것도 아직 커널에 로드되지 않았다면, Kubelet (>= v1.4)은 파드를 거부할 것이다. /sys/kernel/security/apparmor/profiles 파일을 확인해 보면 노드에 어떤 프로파일이 로드되었는지 확인할 수 있다. 예를들어,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | root@zerobig-vm-u:~# cat /sys/kernel/security/apparmor/profiles | sort /sbin/dhclient (enforce) /usr/bin/evince (enforce) /usr/bin/evince-previewer (enforce) /usr/bin/evince-previewer//sanitized_helper (enforce) /usr/bin/evince-thumbnailer (enforce) /usr/bin/evince-thumbnailer//sanitized_helper (enforce) /usr/bin/evince//sanitized_helper (enforce) /usr/lib/NetworkManager/nm-dhcp-client.action (enforce) /usr/lib/NetworkManager/nm-dhcp-helper (enforce) /usr/lib/connman/scripts/dhclient-script (enforce) /usr/lib/cups/backend/cups-pdf (enforce) /usr/lib/lightdm/lightdm-guest-session (enforce) /usr/lib/lightdm/lightdm-guest-session//chromium (enforce) /usr/lib/snapd/snap-confine (enforce) /usr/lib/snapd/snap-confine//mount-namespace-capture-helper (enforce) /usr/sbin/cups-browsed (enforce) /usr/sbin/cupsd (enforce) /usr/sbin/cupsd//third_party (enforce) /usr/sbin/ippusbxd (enforce) /usr/sbin/tcpdump (enforce) docker-default (enforce) webbrowser-app (enforce) webbrowser-app//oxide_helper (enforce) | cs |
노드에 로드되는 프로파일에 대한 보다 자세한 정보는, Setting up nodes with profiles 을 참조한다.
Kubelet 버전이 AppArmor을 지원한다면(>= v1.4), 어떠한 전제조건도 충족시키지 못한 AppArmor 옵션에서는 파드를 거부할 것이다. (비록 다음 릴리즈에서 제거되어질 수도 있기는 하나) 노드의 준비 조건 메시지를 확인하여 AppArmor가 노드에서 지원하는지 확인 할 수 있다.
1 2 | root@zerobig-vm-u:~# kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {.status.conditions[?(@.reason=="KubeletReady")].message}\n{end}' minikube: kubelet is posting ready status | cs |
<참고> 다음 섹션부터는 실습환경 구현이 어려워 원문의 내용을 통해 해석만 하였다.
Securing a Pod
주의: AppArmor는 현재 베타 상태이므로 옵션이 주석(annotation)으로 정의된다. 정식 버전(GA) 자격이 주어지면, 주석은 1등급 필드로 대체 될 것이다.(보다 자세한 내용은 Upgrade path to GA을 참고한다.).
AppArmor 프로파일은 per-container 로 정의된다. 파드 컨테이너와 함께 동작시키기 위해 AppArmor 프로파일을 정의하려면, 파드의 메타데이터에 주석을 추가한다.
container.apparmor.security.beta.kubernetes.io/<container_name>: <profile_ref>
"<container_name>"는 프로파일을 적용하기 위한 컨테이너의 이름이며, "<profile_ref>" 은 적용하려는 프로파일을 정의한다. "profile_ref" 은 다음 중 하나가 될 수 있다.
- "runtime/default" 런타임의 초기 프로파일을 적용함
- "localhost/<profile_name>" "<profile_name>"이름으로 호스트에 로드된 프로파일을 적용함
- "unconfined" 아무런 프로파일이 로드되지 않을 것을 나타냄
전체 자세한 주석과 프로파일 이름 포맷을 확인하려면, API Reference 를 참고한다.
K8S AppArmor 시행은 먼저 모든 전제조건이 충족되는지 확인함으로써 작용한다. 그런 다음 시행을 위한 컨테이너 런타임에 선택 프로파일을 전달해 준다. 전제조건이 충족되지 않으면, 파드는 거부당하고 작동하지 않는다.
적용된 프로파일을 검증하기 위해, 생성된 컨테이너 이벤트에서 AppArmor 보안 옵션 리스트을 찾아볼 수 있다.
1 2 3 | $ kubectl get events | grep Created 22s 22s 1 hello-apparmor Pod spec.containers{hello} Normal Created {kubelet e2e-test-stclair-minion-group-31nt} Created container with docker id 269a53b202d3; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write] | cs |
프로세스 속성을 체크해 봄으로써 컨테이너 루트 프로세스가 올바른 프로파일을 가지고 동작 중인지 직접적으로 검증해 볼 수도 있다.
1 2 | $ kubectl exec <pod_name> cat /proc/1/attr/current k8s-apparmor-example-deny-write (enforce) | cs |
Example
이번 예제는 AppArmor를 지원하는 클러스터가 이미 설치 되어 있음을 가정한다.
먼저, 노드에서 이용하기를 원하는 프로파일을 로드하는 것이 필요하다. 우리가 이용할 프로파일은 모든 쓰기를 거부한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | $ 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 | cs |
다음, 쓰기-거부(deny-write) 프로파일을 가지고 간단한 “Hello AppArmor” 파드를 동작시킬 것이다,
pods/security/hello-apparmor.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 | apiVersion: v1 kind: Pod metadata: name: hello-apparmor annotations: # Tell Kubernetes to apply the AppArmor profile "k8s-apparmor-example-deny-write". # Note that this is ignored if the Kubernetes node is not running version 1.4 or greater. 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" ] | cs |
1 | $ kubectl create -f ./hello-apparmor.yaml | cs |
파드 이벤트를 확인해 보면, “k8s-apparmor-example-deny-write” AppArmor 프로파일을 가지고 생성했던 파드 컨테이너를 볼 수 있을 것이다.
1 2 3 4 5 6 | $ 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 | cs |
프로세스 속성을 체크해 봄으로써 프로파일을 가지고 실제 동작하는 컨테이너를 검증할 수 있다.
1 2 3 | $ kubectl exec hello-apparmor cat /proc/1/attr/current k8s-apparmor-example-deny-write (enforce) | cs |
마자막으로, 파일에 쓰기를 행하여 프로파일을 위반하도록 시도할 경우, 무슨 일이 발생하는지 볼 수 있다.
1 2 3 | $ 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 | cs |
정리를 위해, 로드되지 않았던 프로파일을 정의하도록 시도할 경우, 무슨 일이 발생하는지 살펴보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | $ 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-minion-group-t1f5} Warning AppArmor Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded | cs |
도움을 줄만한 에러 메시지: "Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded" 를 발생시키며 파드의 상태가 실패났음을 주목한다. 또한 이벤트도 동일 메시지를 가지고 기록된다.
Administration
Setting up nodes with profiles
현재 K8S는 노드 상에 AppArmor 프로파일 로딩을 위한 어떤 내제된 메커니즘도 제공하고 있지 않다. 그러나 다음과 같이 프로파일을 설정하기 위한 다양한 방법들이 있다.
- 올바른 프로파일이 로드되었는지 확인하기 위한 DaemonSet을 통해서. 구현 예시는 여기에서 확인 가능 하다.
- 노드 초기화 스크립트 (e.g. Salt, Ansible, etc.) 또는 이미지를 이용하여, 노드 초기화 시
- 예시에서 보이는 바와 같이, SSH를 통해 각 노드에 프로파일을 복사하고 로드시켜 줌으로써
스케줄러는 어떤 노드에 어떤 프로파일이 로드되었는지 인지하지 못한다. 그래서 프로파일 전체 셋이 모든 노드 상에 로드되어져야 한다. 대체가능한 다른 접근 방식은 노드에 각 프로파일 (또는 프로파일 클래스)에 대한 노드 레이블을 추가해 주는 것이다, 그리고 노드에 요구되는 프로파일과 함께 파드가 동작하는지 확인하기 위해 node selector를 이용하는 것이다.
Restricting profiles with the PodSecurityPolicy
PodSecurityPolicy 확장이 사용가능하게 설정되었다면, 클러스터-와이드 AppArmor 제한은 적용될 수 있다. PodSecurityPolicy을 사용가능하게 설정하기 위해, 다음 플래그를 "apiserver" 상에 설정해줘야 한다.
1 | --enable-admission-plugins=PodSecurityPolicy[,others...] | cs |
AppArmor 옵션은 PodSecurityPolicy 상에 주석으로써 정의될 수 있다.
1 2 | apparmor.security.beta.kubernetes.io/defaultProfileName: <profile_ref> apparmor.security.beta.kubernetes.io/allowedProfileNames: <profile_ref>[,others...] | cs |
아무런 정의가 이루어지지 않은 상태인 경우, 기본 프로파일 이름 옵션이 기본으로 컨테이너에게 적용할 프로파일을 정의해 준다. 허용 가능프로파일 이름 옵션은 파드 컨테이너가 참조하여 동작되어지도록 허용한 프로파일 리스트를 정의해 준다. 두개의 옵션이 제공된 경우, 기본값은 허용되어질 것이다. 프로파일은 컨테이너에서와 같이 동일 형식으로 정의된다. 전체 정의를 확인하려면, API Reference 를 확인한다.
Disabling AppArmor
클러스터에서 AppArmor가 이용가능 상태로 되는 것을 원하지 않는다면, 커맨드-라인 플래그를 통해 이용하지 못하도록 만들 수 있다.
1 | --feature-gates=AppArmor=false | cs |
이용불가 상태가 되면, AppArmor 프로파일을 포함한 파드는 "Forbidden" 에러와 함께 확인 실패가 발생할 것이다. 기본으로 Docker는 언제나 비특권 파드(AppArmor 커널이 이용가능으로 설정된 경우)에 대해 "docker-default" 프로파일을 이용가능하게 하여 feature-gate가 이용할 수 없도록 설정된 경우라 하더라도 계속 그리되도록 할 것이다. AppArmor 을 이용가능하지 않도록 설정해 주는 옵션은 AppArmor가 정식 버전(GA)이 되면 제거될 것이다.
Upgrading to Kubernetes v1.4 with AppArmor
클러스터를 1.4로 업그레이드 하는데 있어 AppArmor에서는 필요한 조치는 없다. 그러나, AppArmor 주석이 포함된 기존 파드는 확인(또는 PodSecurityPolicy 승인)을 거키지 않게 될 것이다. 노드 상에 허가된 프로파일이 로드되어 있다면, 악의적인 사용자가 docker-default를 넘어서 파드 특권을 확대하기 위해 허가된 프로파일을 사전 적용 할 수도 있다. 이런한 점에 우려가 생긴다면, "apparmor.security.beta.kubernetes.io" 주석을 포함하는 파드의 클러스터를 제거하는 것이 권고된다.
Upgrade path to General Availability
AppArmor가 정식버전(GA)으로 되면, 현재 주석형태로 된 옵션은 필드로 전환될 것이다. 전환을 거치는 과정에서의 업그레이드와 다운그레이드에 대한 지원은 상당히 미묘한 뉘앙스를 갖는다. 전환이 이루어 질때 상세하게 설명이 이루어 질 것이다. 최소 2번의 릴리즈 동안 필드와 주석 둘을 지원하도록 할 것이다. 그리고 그 이후 최소 2번의 릴리즈 동안 명확하게 주석을 거부할 것이다.
Authoring Profiles
AppArmor 프로파일을 정확하게 정의하는 것은 곤란한 업무일 수 있다. 다행히도 도움이 될 몇 가지 도구들이 있다.
"aa-genprof"
와 "aa-logprof"
는 애플리케이션 활동과 로그를 모니터링하여 프로파일 룰을 생성해주고 취해야 할 조치를 승인해준다. 추가적인 안내는 AppArmor documentation에서 제공된다.- bane 은 간소화한 프로파일 언어를 이용하는 Docker 용 AppArmor 프로파일 생성기다.
프로파일을 생성하기 위해 개발용 워크스테이션에 Docker를 통해 애플리케이션을 동작시키도록 권고되기는 하지만, 파드가 동작하고 있는 K8S 노드상에 도구의 동작을 막는 것은 아무것도 없다.
AppArmor의 문제를 디버깅하기 위해, 특별하게 무엇이 거부되었는지 보기 위해 시스템 로그를 확인할 수 있다. AppArmor는 "dmesg"에 장황한 메시지를 로깅하므로, 보통 시스템 로그 또는 "journalctl"을 통해서 에러가 발견될 수 있다. 추가적인 정보는 AppArmor failures 에서 제공된다.
API Reference
Pod Annotation
컨테이너와 함께 동작하게 될 프로파일을 정의하기
- key: "container.apparmor.security.beta.kubernetes.io/<container_name>"
"<container_name>" 파드의 컨테이너 이름과 매치된다. 파드 내 각 컨테이너에 대해 개별 프로파일이 정의될 수 있다. - value: 프로파일 레퍼런스. 아래 설명한다.
Profile Reference
"runtime/default"
: 기본 런타임 프로파일을 참조한다.AppArmor가 이용가능하도록 설정되어질 필요가 있는 경우를 제외하고, (PodSecurityPolicy 기본 없이) 프로파일을 정의하지 않는 경우와 같다.
Docker의 경우, 비특권 컨테이너에 대해서는 "
docker-default"
프로파일에서 특권 컨테이너에 대해서는 비제한(프로파일 없음)에서 이를 해결한다.
"localhost/<profile_name>"
: 이름으로 노드(로컬호스트)에 로드된 프로파일을 참조한다.- 가능한 프로파일 이름에 관한 자세한 내용은 core policy reference에 있다.
unconfined
: 이는 효과적으로 컨테이너에서 AppArmor를 이용할 수 없도록 한다.
여타 프로파일 참조 포맷은 유효하지 않다.
PodSecurityPolicy Annotations
아무것도 제공되지 않은 경우 컨테이너에 적요할 기본 프로파일을 정의하기
- key: "
apparmor.security.beta.kubernetes.io/defaultProfileName"
- value: 프로파일 레퍼런스, 아래 설명한다.
파드 컨테이너가 정의할 수 있도록 허용된 프로파일 리스트를 정의하기
- key: "
apparmor.security.beta.kubernetes.io/allowedProfileNames"
- value: 콤마로 구분된 프로파일 레퍼런스 리스트 (아래 설명한다.)
- 비록 이스케이프 콤마가 프로파일 이름에서 적법한 캐릭터이기는 하나, 명백하게 여기서는 허용되지 않는다.
What's next
부가적인 리소스들로 다음이 있다.