AppArmor로 컨테이너의 리소스 접근 제한하기
AppArmor로 컨테이너의 리소스 접근 제한하기
이것은 Kubernetes의 안정적인 기능이며, 1.31 릴리스부터 그래왔어요. 더 이상 이 기능을 토글할 수 없습니다(관련 기능 게이트는 제거됨).
이 페이지는 노드에 AppArmor 프로파일을 로드하고 파드에서 그 프로파일을 강제하는 방법을 보여줘요. Kubernetes가 AppArmor로 파드를 구속(confine)할 수 있는 방법에 대해 더 배우려면 파드와 컨테이너용 Linux 커널 보안 제약을 참고해요.
출처: 문서
본문
목표 (Objectives)
- 노드에 프로파일을 로드하는 예시 보기
- 파드에 프로파일을 강제하는 방법 배우기
- 프로파일이 로드되었는지 확인하는 방법 배우기
- 프로파일이 위반될 때 무슨 일이 일어나는지 보기
- 프로파일을 로드할 수 없을 때 무슨 일이 일어나는지 보기
시작하기 전에
AppArmor는 선택적인 커널 모듈이자 Kubernetes 기능이므로, 진행하기 전에 노드에서 지원되는지 확인해요.
- AppArmor 커널 모듈 활성화 — Linux 커널이 AppArmor 프로파일을 강제하려면 AppArmor 커널 모듈이 설치되고 활성화되어 있어야 해요. Ubuntu와 SUSE 같은 여러 배포판은 모듈을 기본으로 활성화하며, 다른 많은 배포판은 선택적 지원을 제공해요. 모듈이 활성화되었는지 확인하려면
/sys/module/apparmor/parameters/enabled파일을 확인해요. AppArmor가 명시적으로 구성된 파드를 허용하기 전에 kubelet이 호스트에서 AppArmor가 활성화되어 있는지 확인해요. - 컨테이너 런타임이 AppArmor 지원 — 모든 일반적인 Kubernetes 지원 컨테이너 런타임은 containerd와 CRI-O를 포함해 AppArmor를 지원해야 해요. 해당 런타임 문서를 참조하고 클러스터가 AppArmor 사용 요구 사항을 충족하는지 확인해요.
- 프로파일 로드됨 — AppArmor는 각 컨테이너가 실행되어야 하는 AppArmor 프로파일을 지정해 파드에 적용돼요. 지정된 프로파일 중 하나라도 커널에 로드되지 않으면 kubelet이 파드를 거부해요. 노드에 로드된 프로파일을 보려면
/sys/kernel/security/apparmor/profiles파일을 확인해요. 노드에 프로파일을 로드하는 자세한 내용은 프로파일이 있는 노드 설정을 참고해요.
파드 보안 설정 (Securing a Pod)
AppArmor 프로파일은 파드 수준 또는 컨테이너 수준에서 지정할 수 있어요. 컨테이너의 AppArmor 프로파일이 파드 프로파일보다 우선해요.
securityContext:
appArmorProfile:
type: <profile_type>
<profile_type>은 다음 중 하나예요.
RuntimeDefault— 런타임의 기본 프로파일 사용Localhost— 호스트에 로드된 프로파일 사용 (아래 참고)Unconfined— AppArmor 없이 실행
AppArmor 프로파일 API에 대한 전체 세부 사항은 AppArmor 구속 지정을 참고해요.
프로파일이 적용되었는지 확인하려면 컨테이너의 루트 프로세스가 올바른 프로파일로 실행 중인지 프로세스의 proc attr을 검사해 확인할 수 있어요.
kubectl exec <pod_name> -- cat /proc/1/attr/current
출력은 대략 이렇게 보여야 해요.
cri-containerd.apparmor.d (enforce)
예시 (Example)
이 예시는 AppArmor 지원으로 클러스터를 이미 설정했다고 가정해요.
먼저 사용할 프로파일을 노드에 로드해요. 이 프로파일은 모든 파일 쓰기 작업을 차단해요.
#include <tunables/global>
profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
#include <abstractions/base>
file,
# Deny all file writes.
deny /** w,
}
프로파일은 어디에 파드가 스케줄링될지 알 수 없으므로 모든 노드에 로드해야 해요. 이 예시에서는 SSH를 사용해 프로파일을 설치할 수 있지만, 다른 접근 방식은 프로파일이 있는 노드 설정에서 논의해요.
# This example assumes that node names match host names, and are reachable via SSH.
NODES=($( kubectl get node -o jsonpath='{.items[*].status.addresses[?(.type == "Hostname")].address}' ))
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
다음으로, deny-write 프로파일로 간단한 "Hello AppArmor" 파드를 실행해요.
apiVersion: v1
kind: Pod
metadata:
name: hello-apparmor
spec:
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: k8s-apparmor-example-deny-write
containers:
- name: hello
image: busybox:1.28
command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
kubectl create -f hello-apparmor.yaml
컨테이너가 실제로 그 프로파일로 실행 중인지 /proc/1/attr/current를 확인해 검증할 수 있어요.
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
spec:
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: k8s-apparmor-example-allow-write
containers:
- name: hello
image: busybox:1.28
command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
EOF
pod/hello-apparmor-2 created
파드가 성공적으로 생성되었지만, 추가 조사해보면 pending에 갇혀 있음을 보여줘요.
kubectl describe pod hello-apparmor-2
Name: hello-apparmor-2
Namespace: default
Node: gke-test-default-pool-239f5d02-x1kf/10.128.0.27
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
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 10s default-scheduler Successfully assigned default/hello-apparmor to gke-test-default-pool-239f5d02-x1kf
Normal Pulled 8s kubelet Successfully pulled image "busybox:1.28" in 370.157088ms (370.172701ms including waiting)
Normal Pulling 7s (x2 over 9s) kubelet Pulling image "busybox:1.28"
Warning Failed 7s (x2 over 8s) kubelet Error: failed to get container spec opts: failed to generate apparmor spec opts: apparmor profile not found k8s-apparmor-example-allow-write
Normal Pulled 7s kubelet Successfully pulled image "busybox:1.28" in 90.980331ms (91.005869ms including waiting)
Event가 이유와 함께 오류 메시지를 제공하며, 구체적인 표현은 런타임에 따라 달라요.
Warning Failed 7s (x2 over 8s) kubelet Error: failed to get container spec opts: failed to generate apparmor spec opts: apparmor profile not found
관리 (Administration)
프로파일이 있는 노드 설정
Kubernetes 1.37은 AppArmor 프로파일을 노드에 로드하는 내장 메커니즘을 제공하지 않아요. 프로파일은 커스텀 인프라스트럭처나 Kubernetes Security Profiles Operator 같은 도구를 통해 로드할 수 있어요.
스케줄러는 어떤 프로파일이 어떤 노드에 로드되었는지 모르므로, 전체 프로파일 집합을 모든 노드에 로드해야 해요. 대안적인 접근 방식은 각 프로파일(또는 프로파일 클래스)에 대해 노드에 노드 라벨을 추가하고, 노드 셀렉터를 사용해 파드가 필요한 프로파일이 있는 노드에서 실행되도록 하는 것이에요.
프로파일 작성
AppArmor 프로파일을 올바르게 지정하는 것은 까다로운 일이 될 수 있어요. 다행히 그것을 돕는 도구가 몇 가지 있어요.
- aa-genprof와 aa-logprof는 애플리케이션의 활동과 로그를 모니터링하고 그것이 취하는 동작을 수용해 프로파일 규칙을 생성해요.
- bane는 단순화된 프로파일 언어를 사용하는 Docker용 AppArmor 프로파일 생성기예요.
AppArmor 문제를 디버깅하려면 시스템 로그를 확인해 구체적으로 무엇이 거부되었는지 볼 수 있어요. AppArmor는 상세 메시지를 dmesg에 기록하며, 오류는 보통 시스템 로그에서 또는 journalctl을 통해 찾을 수 있어요. 자세한 정보는 AppArmor 실패에서 제공돼요.
AppArmor 구속 지정
보안 컨텍스트 내의 AppArmor 프로파일
컨테이너의 securityContext 또는 파드의 securityContext에 appArmorProfile을 지정할 수 있어요. 프로파일이 파드 수준에서 설정되면, 그것은 파드의 모든 컨테이너(init, sidecar, 임시 컨테이너 포함)에 대한 기본 프로파일로 사용돼요. 파드와 컨테이너 AppArmor 프로파일 둘 다 설정되면 컨테이너의 프로파일이 사용돼요.
AppArmor 프로파일은 2개의 필드를 가져요.
type(필수) — 적용될 AppArmor 프로파일의 종류를 나타내요. 유효한 옵션:Localhost— 노드에 사전 로드된 프로파일(localhostProfile로 지정)RuntimeDefault— 컨테이너 런타임의 기본 프로파일Unconfined— AppArmor 강제 없음
localhostProfile— 작업할 노드에 로드된 프로파일의 이름. 프로파일은 동작하려면 노드에 미리 구성되어 있어야 해요. 이 옵션은type이Localhost인 경우에만 제공되어야 하고, 반드시 제공해야 해요.
다음 단계
추가 리소스: