[CKS] Minimize Microservice Vulnerabilities (6) - Container Sandboxing

Kubernetes 보안을 위한 컨테이너 격리 기술을 소개합니다. Seccomp, AppArmor, gVisor, Kata Containers 등 샌드박싱 방식과 RuntimeClass를 통한 런타임 구성 방법을 재정리했습니다.

개요

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

VM과 Container의 차이점

모든 VM은 물리적 인프라를 기반으로 구축됩니다.

Notion Image

Container는 애플리케이션 추상화를 통해 더 가벼운 격리 공간을 제공합니다.

하지만 Host OS를 공유하는만큼 A 컨테이너에서 취약점이 악용되면 B 컨테이너까지 영향받을 수 있습니다.

컨테이너 프로세스 격리

호스트는 모든 컨테이너 프로세스를 볼 수 있지만, 컨테이너는 자체 격리된 PID Namespace를 유지합니다.

root@ubuntu-server:~# docker run -d --name sleeping-container busybox sleep 1000
e2fd5090c9a51eb7cc91a466871f84adb55c2e6c1cf4ea0028a8


root@ubuntu-server:~# docker exec -ti sleeping-container ps -ef
PID USER     TIME COMMAND
  1  root     0:00 sleep 1000
 11  root     0:00 ps -ef

컨테이너 애플리케이션도 동일하게 사용자 공간에서 호출되기 때문에 Syscall 을 통해 하드웨어에 접근합니다.

하지만 단일 컨테이너로 처리하기에, 모든 Syscall 은 동일한 커널에서 처리됩니다. Dirty Cow 같은 취약점으로 커널을 손상시킨다면 모든 컨테이너를 위험에 노출시키고 Host OS 접근을 허용할 수 있습니다.

Container Sandbox

공유 커널 문제를 완화하기 위해 샌드박싱으로 추가 보호 조치를 적용합니다.

시스템 내 구성 요소를 서로 격리하는 모든 기술을 의미합니다.

Seccomp(보안 컴퓨팅)

Docekr Container가 Syscall 을 하지 못하도록 기본 Seccomp 프로필로 보호합니다.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "execve",
        "brk",
        "access",
        "capset",
        "clone"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

AppArmor

Seccomp가 Syscall 을 막는다면 AppArmor 는 파일, 디렉토리(리소스)를 제어합니다.

AppArmor 는 광범위한 허용 정책으로 특정 작업을 제한합니다.

profile apparmor-deny-write flags=(attach_disconnected) {
    # Deny all file writes to /proc.
    deny /proc/* w,
}

참고

완벽한 샌드박싱 방식은 없습니다. 최적의 구성이란 요구 사항과 애플리케이션에 따라 언제든지 달라질 수 있음을 유념합니다. 위험을 최소화한다는 점을 명확하게 확인합니다.

gVisor

컨테이너 격리를 강화하기 위해 Google에서 만든 gVisor를 확인합니다.

컨테이너가 리눅스 커널에 도달하기 전 훨씬 더 철저하게 격리될 수 있습니다.

gVisor 아키텍쳐

gVisor는 강력한 격리 계층을 위해 두 가지 주요 구성요소로 구성됩니다.

Notion Image

Sentry

컨테이너 환경에 특화된 애플리케이션 레벨 커널입니다.

컨테이너 최적화 설계로 Sentry는 전체 Linux 커널 대비 제한된 기능을제공합니다.

Gofer

파일 접근 요청시 Sentry는 Syscall 요청을 커널로 직접 전달하지 않습니다. 대신 Gofer라는 전용 프로세스와 통신하며, Gofer는 파일 프록시 역할을 수행합니다.

Gofer는 컨테이너화된 애플리케이션이 시스템 파일에 접근하기 위해 필요한 로직을 처리합니다.

Sentry와 Gofer는 Isolation(격리)하여 애플리케이션 로직과 파일 시스템 로직을 구분했습니다.

역할을 정리하면 다음과 같습니다.

또한 gVisor는 운영 체제 네트워크 코드 대신 자체 네트워크 스택을 사용합니다.

이를 통해 커널 네트워크와 격리되어 커널을 공유해서 생기는 문제를 최소화합니다.

gVisor 이점과 단점

컨테이너에는 자체적으로 Host OS 커널과 격리된 gVisor 커널이 실행되므로 상대적으로 안전하며 Attack Surface도 최소화할 수 있습니다.

gVisor 인스턴스 하나가 손상돼도 다른 컨테이너에는 영향 없음.

하지만 Sentry, Gofer의 격리 구조는 모든 애플리케이션과 완벽히 호환되는 건 아닙니다. 중간자로 인해 약간의 성능 저하, 애플리케이션 호환성 문제가 발생할 수 있으므로 테스트하는 것이 중요합니다.

kata Containers

Kata Container는 각 컨테이너 별 경량 VM을 사용합니다.

여러 애플리케이션이 동일한 운영 체제를 공유하는 방식과 달리 Kata Containers는 모든 컨테이너에 전용 커널을 할당합니다. 이런 격리를 통해 컨테이너의 내부 오류가 해당 컨테이너에만 영향을 미치게 만들 수 있습니다.

kata는 추가 메모리 공간과 컴퓨팅 용량을 요구하지만 VM을 그냥 할당하는 것보다는 적습니다.

Hardware Requirements

Kata는 하드웨어 가상화에 의존합니다. 따라서 클라우드 환경을 활용하기 어렵습니다. 대부분의 클라우드는 중첩 가상화(VM안에 VM) 구성을 지원하지않습니다. 가능하다고 하더라도 많은 수동 구성이 필요하며 최적화하기 어렵습니다.

Runtime Classes

gVisor, kata Container가 어떻게 배포되는지 확인합니다.이를 이해하려면 Docker 내부에서 컨테이너가 실제로 생성되기까지 여러 컴포넌트를 알아야합니다.

내부 구조

run의 역할

runc는 OCI(Open Containers Initiative)에서 정의한 표준을 구현하는 기본 컨테이너 런타임

runc를 통해 Docker 관리 기능 의존없이 CLI를 사용하여 직접 생성 가능 가능합니다

대체 컨테이너 런타임

샌드박싱 기술향승으로 특수 컨테이너 런타임 존재. Docker 실행 시 런타임 지정 가능.

Kubernetes에서 Runtime 사용하기

gVisor를 사용하여 runsc 런타임으로 컨테이너 생성 방법을 정리했습니다.

Runtime class 생성

RuntimeClass 객체 생성을 위해 중요한 필드는 2가지입니다.

apiVersion: node.k8s.io/v1beta1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc

$ kubectl create -f gvisor.yaml
# Expected output:
# runtimeclass.node.k8s.io/gvisor created

gVisor Runtime으로 Pod 배포하기

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: nginx
  name: nginx
spec:
  runtimeClassName: gvisor
  containers:
    - image: nginx
      name: nginx

아래 명령어를 통해 Nginx 프로세스를 확인합니다.

node01:~# pgrep -a nginx