[CKS] Kubernetes 시스템을 강화하는 방법(3)

Kubernetes v1.31부터 GA된 AppArmor를 활용해 컨테이너의 파일 접근, 네트워크, capabilities 등 리소스 접근을 제어하고 보안을 강화하는 방법을 정리했습니다.

[CKS] Kubernetes 시스템을 강화하는 방법(3)
Photo by Kenny Eliason / Unsplash

개요

CKS 자격증을 위해 아래 내용을 정리했습니다.

AppArmor

애플리케이션의 시스템 리소스 접근을 제한하도록 설계된 보안 모듈입니다. Seccomp 프로필이 제공하는 것 이상의 컨테이너 보안을 강화하며 Attack Surface를 줄일 수 있습니다.

Seccomp는 시스템 호출을 제한하는 데 효과적입니다.

AppAmor는 파일이나 디렉터리와 같은 리소스 접근(파일, 네트워크, capabilities 등)을 제어합니다.

AppArmor 상태

AppArmor는 애플리케이션이 특정 파일, 디렉토리 권한, 네트워크 설정만 사용하게 제어합니다.

대부분의 Linux 배포판에 적용되어 있습니다.

$ systemctl status apparmor

# AppArmor 커널 모듈이 로드되었는지 확인 가능 
$ cat /sys/module/apparmor/parameters/enabled
cat /sys/kernel/security/apparmor/profiles

# AppArmor 프로필 검사
docker-default (enforce)
/usr/sbin/tcpdump (enforce)
/usr/sbin/ntpd (enforce)
/usr/lib/snapd/snap-confine (enforce)
/usr/lib/snapd/snap-confine/mount-namespace-capture-helper (enforce)
/usr/lib/connman/scripts/dhclient-script (enforce)
/usr/lib/NetworkManager/nm-dhcp-helper (enforce)
/usr/lib/NetworkManager/nm-dhcp-client.action (enforce)
/sbin/dhclient (enforce)
/usr/bin/man (enforce)
/usr/bin/man_filter (enforce)

AppArmor 프로필 생성 및 사용 방법

AppArmor 프로필은 Linux 기능, 네트워크 액세스 및 파일 권한과 같이 애플리케이션이 액세스할 수 있는 리소스를 정의하는 텍스트 파일입니다.

- 전체 파일 시스템에 쓰기를 거부하는 프로필은 다음과 같습니다.
    - `flags=(attach_disconnected)` **컨테이너 환경에서 AppArmor 작동하도록** 하는 플래그
profile apparmor-deny-write flags=(attach_disconnected) {
    file,
    # Deny all file writes.
    deny /** w,
}
  • 읽기 전용으로 마운트 되는 것을 방지합니다.
profile apparmor-deny-remount-root flags=(attach_disconnected) {
  # Deny remounting the root filesystem as read-only.
  deny mount options=(ro, remount) -> /,
}

설정이 잘못되면 예기치 않은 동작이 발생하거나 보안이 저하될 수 있습니다.

프로덕션 환경에 배포하기 전에 항상 테스트가 필요합니다.

AppArmor 프로필 상태 확인

  • aa-status 를 사용하여 AppArmor 상태를 확인할 수 있습니다.
    • AppArmor 모듈 로드 여부
    • 로드된 프로필 개수
    • enforce 모드 → 실제 차단
    • complain 모드 → 로깅만, 차단 안 함
    • Unconfined 모드 → 로깅, 차단 모두 안함.
$ aa-status

apparmor module is loaded.
12 profiles are loaded.
12 profiles are in enforce mode.
    /sbin/dhclient
    /usr/bin/man
    /usr/lib/NetworkManager/nm-dhcp-client.action
    /usr/lib/NetworkManager/nm-dhcp-helper
    ...
    /usr/sbin/tcpdump
    docker-default
    man_filter
    man_groff
0 profiles are in complain mode.
11 processes have profiles defined.
11 processes are in enforce mode:
    /sbin/dhclient (621)
    docker-default (3970)
    docker-default (4025)
    docker-default (9853)
    docker-default (9964)
0 processes are in complain mode.
0 processes are 'unconfined' but have a profile defined.

더 자세한 내용은 모범 사례를 참고하세요.

AppArmor 프로필 생성 방법

AppArmor 테스트를 위한 스크립트입니다.

#!/bin/bash
data_directory=/opt/app/data
mkdir -p "${data_directory}"
echo "=> File created at $(date)" | tee "${data_directory}/create.log"

Apparmor 프로필 생성

수동으로 생성하는 것보다 내장 도구를 사용하여 생성할 수 있습니다.

apt-get install -y apparmor-utils
# 쉘 스크립트 전용 프로파일 생성
aa-genprof /root/add_data.sh
  • aa-genprof /root/add_data.sh 실행
  • 스크립트 실행해보면서 필요한 권한 학습
  • /root/add_data.sh만을 위한 AppArmor 프로필 자동 생성

AppArmor가 자동으로 프로필을 생성합니다.

Writing updated profile for /root/add_data.sh.
Setting /root/add_data.sh to complain mode!


Before you begin, you may wish to check if a profile already exists for the application you wish to confine. See the following wiki page for more information:
https://gitlab.com/apparmor/apparmor/wikis/Profiles
Profiling: /root/add_data.sh


Please start the application to be profiled in another window and exercise its functionality now.


Once completed, select the "Scan" option below in order to scan the system logs for AppArmor events.


For each AppArmor event, you will be given the opportunity to choose whether the access should be allowed or denied.


[(S)can system log for AppArmor events] / (F)inish

aa-status를 통해 프로필이 적용되었는지 확인 가능합니다.

apparmor module is loaded.
13 profiles are loaded.
13 profiles are in enforce mode.
/root/add_data.sh
/sbin/dhclient
/usr/bin/man
/usr/lib/NetworkManager/nm-dhcp-client.action
/usr/lib/NetworkManager/nm-dhcp-helper
...
usr/sbin/tcpdump
docker-default
man_filter
man_groff
0 profiles are in complain mode.
11 processes have profiles defined.
11 processes are in enforce mode.
/root/add_data.sh
/sbin/dhclient (621)
docker-default (3970)
docker-default (4025)
docker-default (9853)
docker-default (9964)
0 processes are in complain mode.
0 processes are unconfined but have a profile defined.

새 프로필은 기존 프로필과 함께 /etc/apparmor.d 디렉토리에 저장됩니다.

cat /etc/apparmor.d/root.add_data.sh
# Last Modified: Mon Mar 22 11:21:42 2021
#include <tunables/global>

/root/add_data.sh {
    #include <abstractions/base>
    #include <abstractions/bash>
    #include <abstractions/consoles>


    deny owner /proc/filesystems r,
    /root/add_data.sh r,
    /usr/bin/bash ix,
    /usr/bin/date mrix,
    /usr/bin/mkdir mrix,
    /usr/bin/tee mrix,
    owner /opt/app/ rw,
    owner /opt/app/data/ w,
    owner /opt/app/data/create.log w,
}

이제 스크립트를 수정해보면 무단 접근을 차단합니다.

  • /opt/app/data → /opt
tee: /opt/create.log: Permission denied
=> File created at Mon 22 Mar 2021 04:04:47 PM EDT

기존 AppArmor Profile 사용하기

자주쓰는 AppArmor 명령어

명령어 설명
aa-status 상태 확인 (로드된 프로파일 목록)
apparmor_parser <profile> 프로파일 로드 (enforce 모드)
apparmor_parser -a <profile> 프로파일 추가/로드 (enforce 모드)
apparmor_parser -q <profile> 프로파일 로드 (quiet, 출력 없이)
apparmor_parser -r <profile> 프로파일 교체 (replace)
apparmor_parser -R <profile> 프로파일 언로드 (remove)
apparmor_parser -C <profile> complain 모드로 로드
aa-enforce <profile> enforce 모드로 전환
aa-complain <profile> complain 모드로 전환
aa-disable <profile> 프로파일 비활성화

현재 로드된 프로필을 제거합니다.

  • (-R은 Remove, -r은 replace)
apparmor_parser -R /etc/apparmor.d/root.add_data.sh
# 재부팅 후에도 비활성화 하려면 심볼릭 링크 생성
# ln -s /etc/apparmor.d/root.add_data.sh /etc/apparmor.d/disable/
  • 시스템 재부팅하면 /etc/apparmor.d/의 프로파일들이 자동 로드됨
  • /etc/apparmor.d/disable/에 링크가 있으면 해당 프로파일은 로드 안 함

Kubernetes에서 AppArmor 사용하기

Kubernetes v1.31 (2024년 8월)부터 GA(stable)되어 사용 가능합니다

사용하기 위해 다음 구성 요소를 검토해야합니다.

  • AppArmor 커널 모듈이 활성화되어 있어야 합니다.
  • 원하는 AppArmor 프로파일이 각 노드에 로드되어 있어야 합니다.
  • 컨테이너 런타임(예: Docker, CRI-O, Containerd)이 AppArmor를 지원해야 합니다.

Pod에 AppArmor 프로필 적용하기

AppArmor 프로필을 등록하기 aa-status로 프로필이 제대로 로드되었는지 확인해야합니다.

  • apparmor-deny-write
$ aa-status

apparmor module is loaded.
13 profiles are loaded.
13 profiles are in enforce mode.
    apparmor-deny-write
    /sbin/dhclient
    /usr/bin/man
    /usr/lib/NetworkManager/mn-dhcp-client.action
    /usr/lib/NetworkManager/mn-dhcp-helper
    ...
    /usr/sbin/tcpdump
    docker-default
    man_filter
    man_groff
0 profiles are in complain mode.
11 processes have profiles defined.
11 processes are in enforce mode.
    /sbin/dhclient (621)
    docker-default (3970)
    docker-default (4025)
    docker-default (9853)
    docker-default (9964)
0 processes are in complain mode.
2 processes are unconfined but have a profile defined.
profile apparmor-deny-write flags=(attach_disconnected) {
    file,
    # Deny all file writes.
    deny /** w,
}

최신 Kubernetes 버전 (+1.31)의 경우 securityContext로 지정 가능합니다.

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-sleeper
spec:
  containers:
    - name: ubuntu-sleeper
      image: ubuntu
      command: ["sh", "-c", "echo 'Sleeping for an hour!' && sleep 1h"]
      securityContext:
        appArmorProfile:
          type: Localhost
          localhostProfile: apparmor-deny-write

테스트해보면 실패하는 것을 확인 가능합니다.

kubectl exec -ti ubuntu-sleeper -- touch /tmp/test

touch: cannot touch '/tmp/test': Permission denied
command terminated with exit code 1

Linux Capabilities

지금까지 정리한 내용을 종합하여 Kubernetes Pod에 Linux 기능을 추가하는 방법을 정리했습니다. Seccomp를 Unconfined 모드로 수정해도 차단되는 것을 확인했는데, 이는 여러 레이어로 구성되어있기 때문입니다.

이러한 동작은 k8s pod에도 적용되며 루트(UID 0)으로 실행되어도 제한될 수 있다는 점을 시사합니다.

  • SeccompAppArmorCapabilities

간단한 예시입니다.

  • unconfined 로 수행했음에도 날짜 변경엔 실패함.

리눅스 프로세스가 서로 다른 권한 수준에서 어떻게 작동하는지 이해해야합니다.

docker run -it --rm --security-opt seccomp=unconfined docker/whalesay /bin/sh
# date -s '19 APR 2012 22:00:00'
date: cannot set date: Operation not permitted
Thu Apr 19 22:00:00 UTC 2012


kubectl run --rm -it ubuntu-sleeper --image=ubuntu -- bash

Linux 프로세스 권한 이해하기

Notion Image

리눅스 커널 2.2 이전에는 프로세스가 다음과 같이 분류되었습니다.

  • 권한 있는 프로세스: 루트 사용자(UID 0)에 의해 실행되며 많은 커널 권한 검사를 우회합니다.
  • 비권한 프로세스: 루트 사용자가 아닌 사용자가 실행하며 다양한 커널 제한을 받습니다.

리눅스 커널 2.2부터 슈퍼유저 권한은 capabilities 라는 개별 단위로 분리되었습니다. 이로인해 루트 권한으로 실행하더라도 특정 권한만 부여할 수 있습니다.

Root 권한은 잘게 쪼개졌으며 프로세스마다 필요한 것만 부여합니다.

  • capabilities는 커널 레벨에서 강제됨
  • 프로세스가 자기한테 없는 capability를 임의로 얻을 수 없음
  • root여도 부여받지 않은 capability는 사용 불가

간단하게 나누면 다음과 같습니다.

호스트의 root:

  • 기본적으로 모든 capabilities 보유
  • CAP_CHOWN, CAP_SYS_TIME 등 전부 가능

컨테이너의 root:

  • Docker/Kubernetes가 일부만 부여
  • 기본적으로 14개 정도만 허용
  • CAP_SYS_TIME, CAP_SYS_BOOT 같은 위험한 건 기본 제외

getcaps 명령을 통해 명령어엪ㄹ요한 기능을 확인 할 수 있습니다.

getpcaps $$ # 현재 쉘의 capabilities 확인 가능

getcap /usr/bin/ping
# 출력: /usr/bin/ping = cap_net_raw+ep

ps -ef | grep /usr/sbin/sshd | grep -v grep # PID 찾기
getpcaps 779 # PID로 지정

Default Capabilities in Linux

컨테이너 런타임이 새 컨테이너를 생성할 때 기본으로 부여할 capabilities 목록

func DefaultCapabilities() []string {
    return []string{
        "CAP_CHOWN",
        "CAP_DAC_OVERRIDE",
        "CAP_FOWNER",
        "CAP_MKNOD",
        "CAP_NET_RAW",
        "CAP_SETGID",
        "CAP_SETUID",
        "CAP_SETFCAP",
        "CAP_SETPCAP",
        "CAP_NET_BIND_SERVICE",
        "CAP_SYS_CHROOT",
        "CAP_KILL",
        "CAP_AUDIT_WRITE",
    }
}

만약 조정하려는 경우엔 securityContext 로 capabilites를 전달해야합니다.

  • Kubernetes의 경우
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx
    securityContext:
      capabilities:
        add:
          - SYS_TIME      # 시스템 시간 변경 허용
          - NET_ADMIN     # 네트워크 설정 허용
        drop:
          - CHOWN         # 파일 소유권 변경 금지
          - KILL          # 프로세스 kill 금지
  • Docker의 경우
# 추가
docker run --cap-add SYS_TIME nginx

# 제거
docker run --cap-drop CHOWN nginx

# 전부 제거 후 필요한 것만
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx

하지만 컨테이너의 권한을 수정하면 호스트 시스템이 보안 위험에 노출될 수 있습니다. 항상 필요한 권한만 부여하고 최소 권한 원칙을 준수해야합니다.