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

쿠버네티스 클러스터의 최소 권한 원칙을 구현하기 위해 시스템 호출을 제한하는 seccomp 기능을 설정하는 방법을 소개합니다. strace와 Tracee를 통한 시스템 호출 분석부터 Docker 기본 프로파일 적용, 커스텀 seccomp 프로필 생성까지 다룹니다.

[CKS] Kubernetes 시스템을 강화하는 방법(2)
Photo by Ricardo Arce / Unsplash

개요

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

최소 권한 원칙

이전 글에선 명시적인 접근 제어, 인증, 암호화를 수행했습니다.

명시적인 제어 외 시스템에서 묵시적으로 수행되어야하는 내용을 정리했습니다.

쿠버네티스 클러스터와 같은 컴퓨터 시스템의 최소 권한 표준입니다.

보안 조치 설명
노드 접근 제한 무단 수정을 방지하기 위해 노드에 사용자 권한이 제한되어 있는지 확인하십시오.
역할 기반 접근 제어(RBAC) 클러스터 내 사용자 및 서비스에 대한 정확한 접근 권한을 정의합니다.
사용되지 않는 패키지를 제거 더 이상 필요하지 않은 소프트웨어를 제거하여 시스템을 최신 상태로 유지하십시오.
네트워크 접근 제한 구성 요소 간 네트워크 통신을 제한하여 Attack surface를 줄이십시오.
커널 모듈 제한 필수 커널 모듈만 로드하고 불필요한 모듈은 차단합니다.
열린 포트 수정 무단 침입을 방지하기 위해 개방된 포트를 식별하고 보안을 강화하십시오.

Linux Syscalls

Linux 커널은 Kernel Space라는 전용 메모리 공간에서 시스템 리소스를 관리합니다.

  • 커널 코드, 장치 드라이버 및 관련 확장 기능이 포함

사용자 애플리케이션(C, Java, Python)은 사용자 공간에서 실행됩니다. Kernel Space에서 커널은 하드웨어와 애플리케이션 간의 원할한 통신을 지원합니다.

Notion Image

시스템 호출 방식

syscall은 User Space에서 실행되는 애플리케이션이 커널에 서비스를 요청할 수 있도록 합니다 예를 들면 애플리케이션이 디스크에 저장된 파일을 열어야 할 때 하드웨어에 직접 접근할 수 없으므로 커널에 필요한 작업을 수행하도록 지시해야 합니다. 즉 시스템 호출 명령어에 따라 애플리케이션은 동작합니다

strace 시스템 호출 추적

프로세스가 수행하는 syscall을 추적할 수 있는 방법입니다. strace 는 기본적으로 제공되는 툴 입니다.

strace는 애플리케이션이 호출하는 시스템 호출과 애플리케이션에 전달되는 신호를 추적합니다.

앞에 strace 명령을 사용합니다.

$ strace touch /tmp/error.log

execve("/usr/bin/touch", ["touch", "/tmp/error.log"], 0x7ffce8f874f8 /* 23 vars */) = 0
...
[Output Truncated]
  • execve 는 프로그램을 실행하는데 사용
  • 명령어 실행 파일의 절대 경로
  • 실제명령어와 파일 경로
  • /* 23 vars */ 호출이 23개의 환경 변수를 상속받음
    • 부모 프로세스로부터 23개의 환경 변수(예: PATH, HOME, USER, SHELL 등)를 상속
strace ls /root 2>&1 | grep acce
$ strace -c touch /tmp/error.log # -c를 통해 요약 가능

% time     seconds  usecs/call  calls  errors syscall
------ ----------- ----------- ------ ------ -----------
  0.00      0.000000        0      1      0  read
  0.00      0.000000        0      6      0  close
  0.00      0.000000        0      2      0  fstat
  0.00      0.000000        0      5      0  mmap
  0.00      0.000000        0      4      0  mprotect
  0.00      0.000000        0      1      0  munmap
  0.00      0.000000        0      3      0  brk
  0.00      0.000000        0      3      3  access
  0.00      0.000000        0      1      0  dup2
  0.00      0.000000        0      1      0  execve
  0.00      0.000000        0      1      0  arch_prctl
  0.00      0.000000        0      1      0  openat
  0.00      0.000000        0      1      0  utimensat
------ ----------- ----------- ------ ------ -----------
100.00      0.000000       32      3 total

더 자세한 내용은 리눅스 커널 문서를 참고하세요

AquaSec Tracee

eBPF(Extended Berkeley Packet Filter)를 활용하여 시스템 호출을 추적하는 오픈소스 도구로 Aqua Security에서 개발했습니다.

eBPF의 특징인 커널 수정이나 추가 모듈을 로드하지 않고 커널 공간에서 직접 프로그램을 실행합니다. 최소한의 오버헤드로 운영 체제 동작을 모니터링하고 의심스러운 활동을 감지할 수 있도록 지원합니다.

Tracee를 Docker 컨테이너로 실행

Docker로 실행하면 종속성, 환경 설정을 매우 쉽게 구성할 수 있습니다. Tracee가 컨테이너로 실행될 때 eBPF 프로그램을 컴파일하고 기본적으로 출력을 /tmp/tracee 디렉터리에 알아서 저장합니다.

그 외에도 마운트가 필요한 디렉토리는 다음과 같습니다.

  • /tmp/tracee → 컴파일된 eBPF 프로그램 저장/캐싱 용도
  • /lib/modules, /usr/src → eBPF 프로그램 컴파일에 필요한 소스 용도(읽기 전용 마운트)
    • /lib/modules 현재 커널 모듈 정보
    • /usr/src 커널 헤더 파일
$ docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules:ro \
  -v /usr/src:/usr/src:ro \
  -v /tmp/tracee:/tmp/tracee \
  aquasec/tracee:0.4.0 --trace comm=ls
  
TIME(s)      UID    COMM    PID    TID    RET
1263.457188  0      ls      27461  27461  -2
1263.457218  0      ls      27461  27461  -2
1263.457238  0      ls      27461  27461  0
...
[output truncated]
  • 커널에 훅(hook)을 걸어두고 이벤트가 발생할 때 감지하는 방식

새 프로세스에 대한 시스템 호출 추적

sudo docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules:ro \
  -v /usr/src:/usr/src:ro \
  -v /tmp/tracee:/tmp/tracee \
  aquasec/tracee:0.4.0 --trace pid=new # container=new

새 컨테이너에 대한 시스템 호출 추적

sudo docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules:ro \
  -v /usr/src:/usr/src:ro \
  -v /tmp/tracee:/tmp/tracee \
  aquasec/tracee:0.4.0 --trace container=new

Tracee를 Docker 컨테이너로 실행하면 종속성 관리를 간소화하면서 시스템 활동을 효과적으로 추적 가능합니다.

seccomp를 사용한 시스템 호출 제한

앞서 syscalls 에 대한 추적하는 방법을 살펴봤습니다.

다음으로 애플리케이션이 호출할 수 있는 syscall 을 제한하여 필수적인 시스템 호출만 설정 가능합니다.

  • touch 조차도 많은 syscall 을 수행하는 것 확인 가능
$ strace -c touch /tmp/error.log

% time     seconds  usecs/call     calls    errors  syscall
-------  -----------  -----------  --------  -------  ----------------
  0.00    0.000000        0     1              1    read
  0.00    0.000000        0     6              0    close
  0.00    0.000000        0     2              0    fstat
  0.00    0.000000        0     5              0    mmap
  0.00    0.000000        0     4              0    mprotect
  0.00    0.000000        0     1              0    munmap
  0.00    0.000000        0     3              0    brk
  0.00    0.000000        0     3              3    access
  0.00    0.000000        0     1              0    dup2
  0.00    0.000000        0     1              0    execve
  0.00    0.000000        0     1              0    arch_prctl
  0.00    0.000000        0     3              0    openat
  0.00    0.000000        0     1              0    utimensat
-------  -----------  -----------  --------  -------  ----------------
100.00    0.000000      32     3    total

무제한 시스템 호출 접근을 허용하면 악용 위험이 증가합니다.

2016년의 Dirty COW 취약점은 ptrace 시스템 호출을 악용하여 읽기 전용 파일에 쓰기를 수행함으로써 권한 상승 및 컨테이너 탈출을 초래했습니다.

seccomp

리눅스 커널은 기본적으로 모든 사용자 공간 프로그램(애플리케이션)이 모든 시스템 호출할 수 있도록 허용합니다.

seccomp는 커널 수준 기능으로 허용되는 시스템 호출로 구성하기 위해 허용합니다.

  • 옵션들이 y로 설정되어있으면 seccomp를 지원
grep -i seccomp /boot/config-$(uname -r)
CONFIG_HAVE_ARCH_SECCOMP_FILTER=y
CONFIG_SECCOMP_FILTER=y
CONFIG_SECCOMP=y

Seccomp가 동작하는지는 /proc/1/status 에서 확인할 수 있습니다.

$ cat /proc/1/status | grep Seccomp

Seccomp: 2

Seccomp 값이 의미하는 것은 다음과 같습니다.

  • 0 → seccomp 비활성화 (제한 없음)
  • 1 → strict 모드 (매우 제한적 - Read Write Exit Sigreturn 4가지만 허용)
  • 2 → filter 모드 (프로파일 기반 필터링 적용 중) ← Docker 기본값

Docker는 호스트가 Seccomp를 지원하는 경우 기본 Seccomp 필터를 자동으로 적용합니다.

기본 필터는 약 60개의 시스템 호출을 허용하는 JSON 문서로 정의됩니다.

Default Docker Seccomp 프로필

Docker 엔진에서 내부적으로 사용하는 seccomp를 살펴보면 ptrace같은 위험한 시스템 호출은 자동으로 차단되어있습니다.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "arch_prctl",
        "brk",
        "capget",
        "capset",
        "mkdir",
        "close",
        "execve",
        "...",
        "clone"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

아래 3가지 내용을 포함합니다.

  • 아키텍쳐 정의 → CPU 아키텍쳐
  • 시스템 호출 → 시스템 호출 이름과 허용되는 동작을 나열
  • 기본 동작 → 명시적으로 나열되어있지 않다면 어떻게 처리할지 정의

어떻게 동작하는지는 action을 참고합니다.

  • 화이트리스트 방식
    • "defaultAction": "SCMP_ACT_ERRNO"
    • "action": "SCMP_ACT_ALLOW" → 정의되어있다면 허용
  • 블랙리스트 방식
    • "defaultAction": "SCMP_ACT_ALLOW"
    • "action": "SCMP_ACT_ERRNO" → 정의되어있다면 차단

x86에서 Docker의 기본 Seccomp 프로필은 시스템 시간 조정, 파일 시스템 마운트, 커널 모듈 로딩과 같은 기능과 관련된 약 60개의 시스템 호출을 차단합니다.

Docker Container에서 아래 명령어를 수행해보면 차단된 것을 확인할 수 있습니다.

docker run -it --rm docker/whalesay /bin/sh
#
# date -s '19 APR 2012 22:00:00'
date: cannot set date: Operation not permitted

Custom Seccomp Profile

커스텀한 Seccomp를 사용하고 싶다면 아래 JSON 파일을 만들고 --security-opt 옵션을 통해 지정합니다

  • --security-opt: seccomp, apparmor, label 을 어떻게 설정할지 지정하는 플래그
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "arch_prctl",
        "brk",
        "capget",
        "capset",
        "close",
        "execve",
        "clone"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}
docker run --security-opt seccomp=/path/to/profile.json ...

옵션 값은 다음과 같습니다. 완전 비활성화하는건 매우 취약해지기때문에 권장하지 않습니다.

# seccomp 프로파일 지정
--security-opt seccomp=/path/to/profile.json

# seccomp 완전 비활성화 (위험!)
--security-opt seccomp=unconfined

# AppArmor 프로파일 지정
--security-opt apparmor=profile_name

# SELinux 라벨 설정
--security-opt label=type:container_t

Kubernetes에서 Seccomp 구현

amicontained 오픈소스로 차단되는 시스템 호출을 확인할 수 있습니다.

docker run r.j3ss.co/amicontained amicontained
Container Runtime: docker
Has Namespaces:
    pid: true
    user: false
AppArmor Profile: docker-default (enforce)
Capabilities:
    BOUNDING -> chown dac_override fowner fsetid kill setgid setuid setpcap net_bind_service net_raw sys_chroot mknod audit_write setcap
Seccomp: filtering
Blocked Syscalls (64):
    MSGRCV SYSCFG SETPGID SETSID USELIB USTAT SYSFS VHAVGUP PIVOT_ROOT _SYSCTL ACCT SETTIMEOFDAY MOUNT UMOUNT2 SWAPON SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME IOPL IOPEM CREATE_MODULE INIT_MODULE DELETE_MODULE GET_KERNEL_SYMS QUERY_MODULE QUOTACLI NESSERVCTL GETPMSG PUTMSG AFS_SYSCALL TUXCALL SECURITY LOOKUP_DCOOKIE CLOCK_SETTIME VSERVER MBIND SET_MEMPOLICY GET_MEMPOLICY KEXEC_LOAD ADD_KEY REQUEST_KEY KEYCTL MIGRATE_PAGES UNSHARE MOVE_PAGES PERF_EVENT_OPEN FANOTIFY_INIT NAME_TO_HANDLE_AT OPEN_BY_HANDLE_AT CLOCK_ADJTIME SETNS PROCESS_VM_READV PROCESS_VM_WRITEV KCMP FINIT_MODULE KEXEC_FILE_LOAD
Looking for Docker.sock
  • 현재 적용된 seccomp 프로파일 상태 → Seccomp: filtering (모드 2)
  • 차단된 syscall 목록 → Block Syscalls (64)
  • 사용 가능한 capabilities
  • 컨테이너 런타임 정보
  • 네임스페이스 정보

컨테이너를 k8s pod로 실행

$ kubectl run amicontained --image=r.j3ss.co/amicontained amicontained -- amicontained
$ kubectl logs amicontained
Container Runtime: docker
Has Namespaces:
  pid: true
  user: false
AppArmor Profile: docker-default (enforce)
Capabilities:
  BOUNDING -> chown dac_override fowner fsetid kill setgid setuid setpcap
  net_bind_service net_raw sys_chroot mknod audit_write setcap
Seccomp: disabled


Blocked Syscalls (21):
  SYSCLOG SETGID SETSID VHANGUP PIVOT_ROOT ACCT SETTIMEOFDAY UMOUNT2 SWAPON
  SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME INIT_MODULE DELETE_MODULE LOOKUP_DCOOKIE
  KEXEC_LOAD FANOTIFY_INIT OPEN_BY_HANDLE_AT FINIT_MODULE KEXEC_FILE_LOAD


Looking for Docker.sock
  • 기본적으로 k8s pod는 Seccomp 필터링을 적용하지 않습니다.
    • 차단되는 시스템 호출 수는 21개로 Docker보단 작음.

Kubernetes Pod에서 Seccomp 활성화하기

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: amicontained
  name: amicontained
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
    - args:
        - amicontained
      image: r.j3ss.co/amicontained
      name: amicontained
      securityContext:
        allowPrivilegeEscalation: false
  • allowPrivilegeEscalation: false : 컨테이너 프로세스가 필요 이상의 추가 권한 얻는 걸 방지
  • kubectl apply -f pod-definition.yaml → Docker 기본 Seccomp 프로파일 제공
    • type: RuntimeDefault
Container Runtime: docker
Has Namespaces:
  pid: true
  user: false
AppArmor Profile: docker-default (enforce)
Capabilities:
  BOUNDING -> chown dac_override fowner fsetid kill setgid setuid setpcap net_bind_service net_raw
Seccomp: filtering


Blocked Syscalls (64):
  SYSCLOG SETPGID SETSID USELIB USTAT SYSFS Vhangup PIVOT_ROOT _SYSCtl ACCT SETTIMEOFDAY MOUNT
  UMOUNT2 SWAPON SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME IOPL CREATE_MODULE INIT_MODULE DELETE_MODULE
  GET_KERNEL_SYMS QUERY_MODULE QUOTACTL NFS_SERVERCTL GETMSG PUTMSG AFS_SYSCALL TUXCALL SECURITY
  LOOKUP_DCOOKIE CLOCK_SETTIME VSERVER MBIND SET_MPOLICY GET_MEMPOLICY KEXEC_LOAD ADD_KEY REQUEST_KEY
  KEYCTL MIGRATE_PAGES UNSHARE MOVE_PAGES PERF_EVENT_OPEN FANotify_INIT NAME_TO_HANDLE_AT OPEN_BY_HANDLE_AT
  CLOCK_ADJTIME SETNS PROCESS_VM_READV PROCESS_VM_WRITEV KCMP FINIT_MODULE KEXEC_FILE_LOAD BPF USERFAULTFD
Looking for Docker.soc

제한없이 실행하는 것도 당연히 가능합니다.

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: amicontained
  name: amicontained
spec:
  securityContext:
    seccompProfile:
      type: Unconfined
  containers:
    - args:
        - amicontained
      image: r.j3ss.co/amicontained
      name: amicontained
      securityContext:
        allowPrivilegeEscalation: false

Pod에 Custom Profile 사용하는 방법

apiVersion: v1
kind: Pod
metadata:
  name: test-audit
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: <path to the custom JSON file>
  containers:
    - command: ["bash", "-c", "echo 'I just made some syscalls' && sleep 100"]
      image: ubuntu
      name: ubuntu
      securityContext:
        allowPrivilegeEscalation: false

사용자 지정 프로필 내에서 defaultAction을 시스템 호출 로깅하게 설정할 수 있습니다.

{
  "defaultAction": "SCMP_ACT_LOG"
}
grep syscall /var/log/syslog
Mar 19 23:53:45 node01 kernel: [ 264.340952] audit: type=1326 audit(1616198025.076:14):  auid=4294967295 uid=0 gid=0 ses=4294967295 pid=8816 comm="runc:[2:INIT]" exe="/" sig=0 arch=c000003e syscall=257 compat=0 ip=0x5642801010aa code=0x7ffc0000
Mar 19 23:53:45 node01 kernel: [ 264.340954] audit: type=1326 audit(1616198025.076:15):  auid=4294967295 uid=0 gid=0 ses=4294967295 pid=8816 comm="runc:[2:INIT]" exe="/" sig=0 arch=c000003e syscall=35 compat=0 ip=0x564280fcc662d code=0x7ffc0000
...

다만 tracee도구로 시스템 호출을 훨씬 쉽게 분석 가능하며 Tracee 같은 도그들로 시스템 호출 요구 사항을 분석한 뒤 필요한 시스템 호출만 허용하는 것이 좋습니다.

sudo docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules:ro -v /usr/src:/usr/src:ro \
  -v /tmp/tracee:/tmp/tracee aquasec/tracee:0.4.0 --trace container=new

모든 시스템 호출 거부하기

  • defaultAction: SCMP_ACT_ERRNO 을 설정합니다.
{
  "defaultAction": "SCMP_ACT_ERRNO"
}

ContainerCannotRun 으로 Pod가 올라가지 않습니다.

NAME             READY   STATUS                RESTARTS   AGE
test-violation   0/1     ContainerCannotRun    0          2m2s

정리

  • Unconfined → seccomp 안 씀(defaultAction으로 구성하는 것과는 별개)
    • Pod는 올라감. 다만 보안에 취약해지는 것
    • defaultAction: SCMP_ACT_ERRNO 은 너무 보수적인 운영과 동일
  • RuntimeDefault → 런타임 기본 프로파일 (Docker 기본값과 유사)
  • Localhost → 내가 만든 커스텀 JSON 프로파일 사용