Linux Containers 심화

Linux 컨테이너 기술 스택을 커널 관점에서 종합적으로 다룹니다. OCI 런타임(runc, containerd, Podman), seccomp/capabilities 보안, OverlayFS 스토리지, CNI 네트워킹, rootless 컨테이너, Kata Containers까지 컨테이너의 전체 아키텍처를 설명합니다.

관련 표준: OCI Runtime Specification 1.0 (컨테이너 런타임), OCI Image Format Specification (이미지 형식) — 리눅스 컨테이너 기술 스택의 표준 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

컨테이너 개요

컨테이너는 운영체제 수준 가상화(OS-level virtualization) 기술로, 하나의 커널 위에서 프로세스를 격리된 환경에서 실행합니다. VM이 하이퍼바이저 위에 게스트 커널을 통째로 올리는 것과 달리, 컨테이너는 호스트 커널을 공유하면서 네임스페이스와 cgroups로 격리와 리소스 제한을 구현합니다.

핵심 커널 프리미티브

컨테이너 기술은 다음 4가지 커널 프리미티브의 조합으로 구현됩니다:

프리미티브역할예시
Namespaces리소스 가시성 격리PID, NET, MNT, USER, UTS, IPC, cgroup, time
cgroups리소스 사용량 제한CPU, memory, I/O, PID 수 제한
seccomp-bpf시스템 콜 필터링허용/차단 syscall 목록 적용
Capabilities권한 세분화CAP_NET_BIND_SERVICE, CAP_SYS_ADMIN 등
네임스페이스/cgroups 상세: 각 프리미티브의 내부 구현은 네임스페이스, cgroups 페이지에서 자세히 다룹니다. 이 페이지에서는 컨테이너가 이들을 어떻게 조합하여 사용하는지에 집중합니다.

VM vs Container 아키텍처

Virtual Machine Hardware Host Kernel + Hypervisor (KVM) Guest VM 1 Guest Kernel Libs / Runtime App A App B Guest VM 2 Guest Kernel Libs / Runtime App C Container Hardware Host Kernel (shared) namespaces + cgroups + seccomp + capabilities Container Runtime (runc / containerd) Container 1 Libs App A Container 2 Libs App B Container 3 Libs App C

컨테이너 기술의 역사

연도기술의미
1979chroot파일시스템 루트 격리의 시작
2002Linux namespacesmount namespace (Linux 2.4.19)
2006cgroups (Google)프로세스 그룹 리소스 제한
2008LXC최초의 완전한 Linux 컨테이너 구현
2013Docker이미지 레이어 + 사용자 경험 혁신
2015OCI 설립런타임/이미지 스펙 표준화
2016runc 1.0OCI 참조 런타임 구현
2017containerd 1.0산업 표준 고수준 런타임
2019cgroup v2통합 계층 구조, PSI 지원
2021Podman 3.0+데몬리스, rootless 네이티브

커널 프리미티브 조합

컨테이너 런타임은 단일 프리미티브가 아닌, 여러 커널 기능을 특정 순서로 조합하여 격리 환경을 구성합니다. 아래 다이어그램은 컨테이너 생성 시 커널 프리미티브가 적용되는 흐름을 보여줍니다.

1. clone() CLONE_NEW* flags 2. Namespaces PID/NET/MNT/USER/UTS/IPC 3. cgroups CPU/Mem/IO/PID 제한 4. pivot_root rootfs 전환 5. Mounts /proc, /sys, /dev 마운트 6. Capabilities bounding set 축소 7. seccomp-bpf syscall 필터 적용 8. exec() 컨테이너 프로세스 실행 컨테이너가 사용하는 8개 네임스페이스 PID NS - 프로세스 ID 격리 (컨테이너 내부 PID 1) NET NS - 네트워크 스택 격리 (독립 인터페이스, 라우팅 테이블) MNT NS - 마운트 포인트 격리 (독립 파일시스템 뷰) USER NS - UID/GID 매핑 (rootless 컨테이너의 핵심) UTS NS - 호스트명/도메인명 격리 IPC NS - SysV IPC, POSIX MQ 격리 cgroup NS - cgroup 뷰 격리 (Linux 4.6+) time NS - CLOCK_MONOTONIC/BOOTTIME 격리 (Linux 5.6+) 각 네임스페이스의 커널 내부 구현은 namespaces.html 참조

seccomp-bpf: 시스템 콜 필터링

seccomp(Secure Computing Mode)은 프로세스가 호출할 수 있는 시스템 콜을 제한하는 커널 기능입니다. 컨테이너 런타임은 seccomp-bpf 필터를 사용하여 위험한 시스템 콜을 차단합니다.

/* 커널의 seccomp 필터 데이터 구조 (include/uapi/linux/seccomp.h) */
struct seccomp_data {
    int   nr;                    /* 시스템 콜 번호 */
    __u32 arch;                  /* AUDIT_ARCH_* 값 */
    __u64 instruction_pointer;   /* 호출 위치 */
    __u64 args[6];              /* 시스템 콜 인자 */
};

/* seccomp 필터 설치 */
struct sock_fprog prog = {
    .len    = ARRAY_SIZE(filter),
    .filter = filter,           /* BPF 필터 배열 */
};
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);

/* 또는 seccomp() 시스템 콜 사용 (Linux 3.17+) */
seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog);
Docker 기본 seccomp 프로파일: Docker는 약 44개의 시스템 콜을 기본 차단합니다. kexec_load, mount, reboot, init_module, ptrace, keyctl 등 호스트에 영향을 줄 수 있는 시스템 콜이 차단 대상입니다. --security-opt seccomp=unconfined로 비활성화 가능하나 보안상 권장하지 않습니다.

Linux Capabilities

전통적 Unix는 root(UID 0)와 non-root만 구분했습니다. Linux capabilities는 root 권한을 약 40개 이상의 세분화된 권한으로 분리하여, 컨테이너에 필요한 최소 권한만 부여할 수 있게 합니다.

/* 컨테이너 런타임의 기본 capability 목록 (Docker/runc 기준) */
/* 유지되는 capabilities */
CAP_CHOWN              /* 파일 소유자 변경 */
CAP_DAC_OVERRIDE       /* 파일 접근 권한 우회 */
CAP_FSETID             /* setuid/setgid 비트 보존 */
CAP_FOWNER             /* 파일 소유자 검사 우회 */
CAP_MKNOD              /* 특수 파일 생성 */
CAP_NET_RAW            /* RAW/PACKET 소켓 */
CAP_SETGID             /* GID 조작 */
CAP_SETUID             /* UID 조작 */
CAP_SETFCAP            /* 파일 capability 설정 */
CAP_SETPCAP            /* 프로세스 capability 전달 */
CAP_NET_BIND_SERVICE   /* 1024 미만 포트 바인딩 */
CAP_SYS_CHROOT         /* chroot() 호출 */
CAP_KILL               /* 프로세스에 시그널 전송 */
CAP_AUDIT_WRITE        /* 감사 로그 작성 */

/* 주요 DROP 대상 capabilities */
CAP_SYS_ADMIN          /* mount, namespace 생성 등 — 가장 위험 */
CAP_SYS_PTRACE         /* ptrace, /proc 접근 */
CAP_SYS_MODULE         /* 커널 모듈 로드/언로드 */
CAP_SYS_RAWIO          /* iopl, ioperm */
CAP_SYS_BOOT           /* reboot() */
CAP_NET_ADMIN          /* 네트워크 구성 변경 */
# capability 확인 도구
$ capsh --print               # 현재 프로세스의 capabilities 출력
$ getpcaps <PID>              # 특정 프로세스의 capabilities 확인
$ cat /proc/<PID>/status | grep Cap   # 커널이 보고하는 capability 비트마스크

# 출력 예시 (Docker 컨테이너 내부)
CapInh: 0000000000000000
CapPrm: 00000000a80425fb      # 허용된 capability 비트마스크
CapEff: 00000000a80425fb      # 유효 capability 비트마스크
CapBnd: 00000000a80425fb      # bounding set
CapAmb: 0000000000000000      # ambient capabilities

OCI (Open Container Initiative)

OCI는 컨테이너 포맷과 런타임의 개방 표준을 정의하는 Linux Foundation 프로젝트입니다. Docker가 2015년에 기증한 libcontainer를 기반으로, 모든 컨테이너 런타임이 따르는 표준 인터페이스를 제공합니다.

OCI Runtime Specification

런타임 스펙은 컨테이너의 설정, 실행 환경, 라이프사이클을 정의합니다. config.json이 핵심 설정 파일입니다.

// OCI config.json 핵심 구조 (간략화)
{
  "ociVersion": "1.0.2",
  "process": {
    "terminal": true,
    "user": { "uid": 0, "gid": 0 },
    "args": ["/bin/sh"],
    "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
    "cwd": "/",
    "capabilities": {
      "bounding": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
      "effective": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"]
    },
    "rlimits": [{ "type": "RLIMIT_NOFILE", "hard": 1024, "soft": 1024 }]
  },
  "root": { "path": "rootfs", "readonly": true },
  "mounts": [
    { "destination": "/proc", "type": "proc", "source": "proc" },
    { "destination": "/dev", "type": "tmpfs", "source": "tmpfs" }
  ],
  "linux": {
    "namespaces": [
      { "type": "pid" }, { "type": "network" },
      { "type": "ipc" }, { "type": "uts" },
      { "type": "mount" }, { "type": "cgroup" }
    ],
    "seccomp": { "defaultAction": "SCMP_ACT_ERRNO", "architectures": ["SCMP_ARCH_X86_64"] },
    "resources": {
      "memory": { "limit": 536870912 },
      "cpu": { "shares": 1024, "quota": 100000, "period": 100000 }
    }
  }
}

OCI 컨테이너 라이프사이클

상태설명전이
creating런타임이 config.json을 파싱하고 환경 준비 중create 명령
created환경 준비 완료, 프로세스 미실행prestart 훅 실행됨
running컨테이너 프로세스 실행 중start 명령
stopped프로세스 종료됨, 리소스 미해제프로세스 exit
(삭제됨)모든 리소스 해제delete 명령

OCI Image Specification

OCI 이미지는 content-addressable 스토리지 모델을 사용합니다. 각 레이어는 SHA256 다이제스트로 식별됩니다.

컨테이너 런타임

컨테이너 런타임은 3계층으로 분류됩니다:

계층역할예시
CRI (인터페이스)오케스트레이터와 런타임 간 gRPC APIKubernetes CRI
High-level이미지 관리, 스냅샷, 네트워킹containerd, CRI-O
Low-level (OCI)실제 컨테이너 생성/실행runc, crun, youki
kubelet Kubernetes CRI containerd High-level Runtime exec shim-runc-v2 Runtime Shim exec runc OCI Runtime Kernel clone() Podman (Daemonless) podman → conmon → runc (데몬 없음) CRI-O kubelet → CRI-O → conmon → runc

runc (OCI 참조 구현)

runc은 Docker가 기증한 libcontainer를 기반으로 한 Go 언어 구현의 OCI 참조 런타임입니다. 컨테이너 생성의 핵심 코드 흐름:

/* runc의 컨테이너 생성 흐름 (Go → 커널 호출) */

// 1. config.json 파싱 → Container 구조체 생성
container := libcontainer.Create(id, config)

// 2. 부모 프로세스에서 clone() + CLONE_NEW* 플래그로 자식 생성
//    (실제로는 /proc/self/exe를 재실행하는 "init" 방식)
cmd := &exec.Cmd{
    Path: "/proc/self/exe",   // runc 바이너리 자체를 다시 실행
    Args: []string{"runc", "init"},
}
cmd.SysProcAttr = &syscall.SysProcAttr{
    Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID |
                syscall.CLONE_NEWNS  | syscall.CLONE_NEWNET |
                syscall.CLONE_NEWIPC | syscall.CLONE_NEWUSER,
}

// 3. 자식 프로세스(runc init)에서:
pivot_root(rootfs, put_old)     // rootfs 전환
mount("proc", "/proc", "proc", 0, "")  // /proc 마운트
prctl(PR_SET_NO_NEW_PRIVS, 1)   // 권한 상승 차단
seccomp(SET_MODE_FILTER, &prog)  // seccomp 필터 적용
execve(entrypoint, args, envp)   // 컨테이너 프로세스 실행
# runc 직접 사용 예시
$ mkdir -p bundle/rootfs
$ docker export $(docker create alpine) | tar -C bundle/rootfs -xf -
$ cd bundle
$ runc spec                     # 기본 config.json 생성
$ runc create mycontainer       # 컨테이너 생성 (created 상태)
$ runc start mycontainer        # 프로세스 시작 (running 상태)
$ runc list                     # 컨테이너 목록 확인
$ runc state mycontainer        # 상태 JSON 출력
$ runc delete mycontainer       # 정리

crun (C 구현)

crun은 runc의 C 언어 대안 구현으로, Red Hat이 개발합니다.

비교 항목runc (Go)crun (C)
메모리 사용~10-15 MB (Go 런타임)~1-2 MB
시작 시간기준~40-50% 빠름
cgroup v2지원네이티브 우선 지원
WASM미지원wasmedge/wasmtime 지원
채택Docker 기본Podman/Fedora 기본

containerd

containerd는 CNCF 졸업 프로젝트로, Docker와 Kubernetes의 핵심 컨테이너 런타임입니다.

# containerd 디버깅 도구
$ ctr images pull docker.io/library/alpine:latest    # 이미지 pull
$ ctr run --rm docker.io/library/alpine:latest test /bin/sh  # 실행
$ ctr containers list                                # 컨테이너 목록
$ ctr tasks list                                     # 실행 중인 태스크

# nerdctl (containerd용 Docker-호환 CLI)
$ nerdctl run --rm -it alpine sh                     # Docker와 동일한 UX

Podman

Podman은 Red Hat이 개발한 데몬리스(daemonless) 컨테이너 엔진입니다. Docker와 달리 중앙 데몬이 없어 각 컨테이너가 독립적인 프로세스 트리를 가집니다.

# Podman rootless 사용 예
$ podman run --rm -it alpine sh                      # root 권한 불필요
$ podman pod create --name mypod -p 8080:80          # Pod 생성
$ podman run --pod mypod nginx                       # Pod에 컨테이너 추가

# systemd 서비스로 관리
$ podman generate systemd --name mycontainer --new > mycontainer.service
$ systemctl --user enable --now mycontainer.service

컨테이너 스토리지 (OverlayFS)

OverlayFS는 컨테이너의 기본 스토리지 드라이버로, 읽기 전용 이미지 레이어 위에 쓰기 가능한 컨테이너 레이어를 오버레이합니다. 커널 3.18에서 mainline에 통합되었습니다.

OverlayFS 레이어 구조

디렉토리역할설명
lowerdir읽기 전용이미지 레이어 (여러 개 콜론으로 연결)
upperdir읽기/쓰기컨테이너의 변경사항이 기록되는 레이어
workdir작업용atomic 연산을 위한 임시 디렉토리
merged통합 뷰lower + upper를 합친 최종 마운트 포인트
merged (통합 뷰) upperdir (RW) - Container Layer lowerdir 3 (RO) - App Layer lowerdir 2 (RO) - Runtime Layer lowerdir 1 (RO) - Base Image Layer 파일 연산 READ : lower에서 검색 → 찾으면 반환 WRITE : copy-up → upper에 기록 DELETE: upper에 whiteout 파일 생성 CREATE: upper에 직접 생성 마운트 예시 mount -t overlay overlay \ -o lowerdir=/img/layer3:/img/layer2:/img/layer1,\ upperdir=/container/upper,\ workdir=/container/work \ /container/merged

Copy-up 메커니즘

lower 레이어의 파일을 수정할 때, OverlayFS는 해당 파일을 upper 레이어로 복사한 뒤 수정합니다. 이를 copy-up이라 합니다.

/* OverlayFS copy-up 핵심 (fs/overlayfs/copy_up.c) */
static int ovl_copy_up_one(struct dentry *parent,
                           struct dentry *dentry,
                           struct path *lowerpath,
                           struct kstat *stat)
{
    /* 1. upper에 임시 파일 생성 (workdir에서) */
    temp = ovl_create_temp(workdir, ...);

    /* 2. lower에서 데이터 복사 */
    ovl_copy_up_data(lowerpath, temp, stat->size);

    /* 3. xattr, 권한, 타임스탬프 복사 */
    ovl_copy_xattr(lowerpath->dentry, temp);
    ovl_set_attr(temp, stat);

    /* 4. atomic rename으로 upper에 배치 */
    vfs_rename(workdir, temp, upperdir, dentry);
    return 0;
}
성능 주의: copy-up은 대용량 파일에서 심각한 지연을 유발할 수 있습니다. 예를 들어, lower에 있는 1GB 데이터베이스 파일을 처음 수정하면 전체 파일이 upper로 복사됩니다. 빈번히 수정되는 파일은 볼륨 마운트(-v)를 사용하여 OverlayFS를 우회하세요.

대안 스토리지 드라이버

드라이버특징사용 사례
overlay2다중 lower 레이어, 커널 4.0+Docker/containerd 기본
devicemapper블록 수준 thin provisioningRHEL 7 레거시
btrfs스냅샷 기반, CoW 파일시스템SUSE 계열
ZFS스냅샷/클론, 압축, 체크섬Ubuntu (실험적)
fuse-overlayfsFUSE 기반 overlayrootless (커널 5.11 이전)

Whiteout / Opaque 디렉토리

OverlayFS에서 lower 레이어는 읽기 전용이므로, 파일을 "삭제"할 때 실제 lower의 파일을 제거하는 것이 아니라 upper 레이어에 whiteout 파일을 생성하여 해당 파일을 가립니다. Whiteout은 character device(major=0, minor=0)로 표현됩니다.

/* fs/overlayfs/overlayfs.h — whiteout 관련 상수 */
#define OVL_WHITEOUT_DEV    MKDEV(0, 0)

/* whiteout 생성 (fs/overlayfs/dir.c) */
static int ovl_whiteout(struct ovl_fs *ofs,
                        struct dentry *upperdir,
                        struct dentry *dentry)
{
    struct dentry *whiteout;
    /* workdir에서 whiteout 장치 파일(0,0) 생성 */
    whiteout = ovl_lookup_temp(ofs, ofs->workdir);
    ovl_do_whiteout(ofs, whiteout);  /* mknod(S_IFCHR, 0/0) */

    /* atomic rename으로 upper에 배치 */
    return ovl_do_rename(ofs, ofs->workdir, whiteout,
                         upperdir, dentry, 0);
}

디렉토리 전체를 삭제한 뒤 같은 이름으로 재생성하는 경우, OverlayFS는 opaque 디렉토리를 사용합니다. upper에 새 디렉토리를 만들고 trusted.overlay.opaque="y" xattr을 설정하면, 이 디렉토리 하위에서는 lower의 동명 디렉토리를 참조하지 않습니다.

삭제 전 upper (비어있음) lower: config.txt data/ README merged: config.txt data/ README rm rm config.txt 후 upper: ✖config.txt (whiteout c0,0) lower: config.txt data/ README merged: data/ README ✖ = whiteout (char device 0/0) | lower의 config.txt는 그대로 존재하지만 merged에서 불가시
# Whiteout 확인 예시
$ ls -la upper/
c--------- 1 root root 0, 0 ... config.txt   # whiteout: char device 0/0

# Opaque 디렉토리 확인
$ getfattr -n trusted.overlay.opaque upper/data/
trusted.overlay.opaque="y"

# merged에서 확인
$ ls merged/
data/  README   # config.txt는 whiteout에 의해 숨겨짐
Tip: 백업 도구(rsync, tar 등)로 upper 디렉토리를 백업할 때, whiteout 장치 파일(0/0)과 opaque xattr을 올바르게 보존해야 합니다. tar --xattrs --xattrs-include='trusted.*' 옵션을 사용하거나, OverlayFS 전용 도구를 활용하세요.

Redirect Dir (디렉토리 Rename)

OverlayFS에서 디렉토리를 rename하면 upper에만 새 디렉토리가 생기고, lower의 원본 디렉토리는 여전히 존재합니다. 이를 올바르게 처리하기 위해 커널 4.10+에서 redirect dir 기능이 도입되었습니다. 새 디렉토리의 trusted.overlay.redirect xattr에 원본 경로를 저장하여, lower의 원본 디렉토리 내용을 새 위치에서 투명하게 참조합니다.

옵션 값설명기본값
redirect_dir=on디렉토리 rename 시 redirect xattr 생성-
redirect_dir=off디렉토리 rename 실패 (EXDEV 반환)-
redirect_dir=follow기존 redirect xattr 해석만, 새로 생성 안 함커널 기본값
redirect_dir=nofollowredirect xattr 무시-
/* 디렉토리 rename 시 redirect lookup 의사코드 */
static int ovl_rename(struct inode *olddir, struct dentry *old,
                     struct inode *newdir, struct dentry *new)
{
    /* 1. old가 디렉토리이고 lower에 존재하면 redirect 설정 필요 */
    if (ovl_type_merge_or_lower(old)) {
        /* 2. new 디렉토리에 redirect xattr 설정 */
        ovl_set_redirect(old, ovl_dentry_get_redirect(old));
        /* trusted.overlay.redirect="/original/path" */
    }
    /* 3. 이후 lookup 시 redirect xattr을 읽어 lower 검색에 사용 */
    return ovl_do_rename(...);
}
보안 주의: redirect_dir=on은 lower 레이어의 임의 디렉토리를 upper에서 참조할 수 있게 하므로, 신뢰할 수 없는 upper 레이어를 마운트할 때 보안 위험이 있습니다. 커널 기본값이 follow인 이유입니다. 다중 사용자 환경에서는 redirect_dir=nofollow를 고려하세요.

Metacopy 최적화

커널 4.19+에서 도입된 metacopy 기능은 copy-up 시 파일 데이터 대신 메타데이터(소유권, 권한, xattr)만 upper에 복사합니다. 실제 데이터 읽기/쓰기가 발생할 때까지 데이터 복사를 지연(lazy copy-up)하여 성능을 대폭 개선합니다.

시나리오기존 copy-upmetacopy개선
chmod 1GB 파일~1GB 복사 + chmod메타데이터만 복사~1000x 빠름
chown 대량 파일전체 데이터 복사xattr만 기록디스크 I/O 극소
touch (타임스탬프)전체 데이터 복사메타데이터만즉시 완료
파일 내용 수정 (write)전체 데이터 복사지연 후 데이터 복사동일 (최초 write 시)
/* metacopy 분기 로직 (fs/overlayfs/copy_up.c) */
static int ovl_copy_up_meta_inode_data(struct ovl_copy_up_ctx *c)
{
    /* metacopy 상태 확인: 메타데이터만 복사된 파일인지 */
    if (ovl_is_metacopy_dentry(c->dentry)) {
        /* 데이터 접근 발생 → 이제 실제 데이터 복사 */
        err = ovl_copy_up_data(&c->lowerpath, &datapath,
                                c->stat.size);
        if (err)
            return err;
        /* metacopy xattr 제거 (이제 전체 복사 완료) */
        ovl_do_removexattr(ofs, upperpath.dentry,
                           OVL_XATTR_METACOPY);
    }
    return 0;
}
# metacopy 활성화 마운트
$ mount -t overlay overlay \
  -o lowerdir=/lower,upperdir=/upper,workdir=/work,metacopy=on \
  /merged

# chmod 후 metacopy xattr 확인
$ chmod 644 /merged/large_file.dat
$ getfattr -n trusted.overlay.metacopy /upper/large_file.dat
trusted.overlay.metacopy   # 값 존재 = 데이터 미복사

# 파일 내용을 수정하면 데이터 copy-up 발생
$ echo "data" >> /merged/large_file.dat
$ getfattr -n trusted.overlay.metacopy /upper/large_file.dat
# xattr 없음 = 전체 데이터 복사 완료

OverlayFS 커널 내부 구조

OverlayFS의 커널 구현은 fs/overlayfs/ 디렉토리에 위치하며, VFS 레이어와 통합되어 upper/lower 레이어를 투명하게 합성합니다.

소스 파일역할
super.c파일시스템 등록, 마운트 옵션 파싱, superblock 초기화
inode.cinode operations (getattr, permission, get_acl 등)
dir.c디렉토리 operations (lookup, mkdir, rmdir, rename, whiteout)
file.c파일 operations (open, read_iter, write_iter, mmap, fsync)
copy_up.ccopy-up 핵심 로직, metacopy 처리
readdir.c디렉토리 읽기, whiteout 필터링, merge sort
namei.c경로 탐색 (redirect, metacopy lookup)
util.c유틸리티 함수 (xattr 조작, dentry 헬퍼)
export.cNFS export / file handle 지원
params.c마운트 파라미터 파싱 (커널 6.5+, 기존 super.c에서 분리)
/* 핵심 자료구조: OverlayFS 파일시스템 인스턴스 (fs/overlayfs/ovl_entry.h) */
struct ovl_fs {
    unsigned int        numlayer;       /* 전체 레이어 수 */
    struct ovl_layer    *layers;        /* 레이어 배열 (upper + lowers) */
    struct ovl_sb       *fs;            /* 레이어별 superblock 정보 */
    struct dentry       *workdir;       /* workdir dentry */
    long                namelen;        /* 파일명 최대 길이 */
    struct ovl_config   config;         /* 마운트 옵션 구조체 */
    const struct cred  *creator_cred;  /* 마운트 수행자 credential */
};

/* OverlayFS inode 확장 */
struct ovl_inode {
    struct ovl_dir_cache *cache;     /* readdir 캐시 */
    const char          *redirect;   /* redirect 경로 */
    struct inode        *upper;      /* upper inode (있으면) */
    struct ovl_path     lowerpath;   /* lower 경로 */
    struct inode        vfs_inode;   /* VFS inode (임베딩) */
    unsigned long       flags;       /* OVL_UPPERDATA, OVL_METACOPY 등 */
};

/* dentry 개인 데이터: upper/lower 매핑 */
struct ovl_entry {
    unsigned            numlower;    /* lower 경로 수 */
    struct ovl_path     lowerstack[];  /* flexible array: lower dentry 배열 */
};
/* 파일 수정 시 VFS → OverlayFS 함수 호출 흐름 */

vfs_open(merged_dentry)
  └─ ovl_open()
       ├─ ovl_maybe_copy_up(dentry, O_WRONLY)
       │    └─ ovl_copy_up_flags()
       │         └─ ovl_copy_up_one()           /* copy-up 수행 */
       │              ├─ ovl_create_temp()       /* workdir에 임시파일 */
       │              ├─ ovl_copy_up_data()      /* 데이터 복사 (또는 metacopy 스킵) */
       │              ├─ ovl_copy_xattr()        /* xattr 복사 */
       │              └─ vfs_rename()            /* atomic으로 upper에 배치 */
       └─ ovl_open_realfile()                    /* upper의 실제 파일 open */
            └─ open_with_fake_path(upper_file)   /* 실제 파일시스템에 위임 */

xattr 기반 메타데이터

OverlayFS는 확장 속성(xattr)을 통해 레이어 간 관계와 파일 상태를 관리합니다. 모든 xattr은 trusted.overlay.* 네임스페이스에 저장됩니다.

xattr용도값 예시
trusted.overlay.opaqueopaque 디렉토리 표시"y"
trusted.overlay.redirect디렉토리 rename 원본 경로"/original/dir"
trusted.overlay.metacopy메타데이터만 copy-up됨 표시(빈 값)
trusted.overlay.impure디렉토리에 copy-up된 항목 존재"y"
trusted.overlay.origincopy-up 원본의 file handle(바이너리 fh)
trusted.overlay.upperupper에서 생성된 파일 표시(빈 값)
trusted.overlay.nlinkhardlink 보정 nlink 값"U+3/L+2"

trusted.* 네임스페이스는 root만 읽기/쓰기 가능하여 일반 사용자의 조작을 방지합니다. 커널 5.11+ rootless 환경에서는 userxattr 마운트 옵션을 사용하여 user.overlay.* 네임스페이스로 매핑합니다.

# xattr 전체 조회
$ getfattr -d -m "trusted.overlay.*" upper/mydir/
# file: upper/mydir/
trusted.overlay.impure="y"
trusted.overlay.opaque="y"

# origin file handle 확인 (바이너리)
$ getfattr -n trusted.overlay.origin -e hex upper/copied_file
trusted.overlay.origin=0x00fb010020...

# userxattr 환경 (rootless, 커널 5.11+)
$ getfattr -d -m "user.overlay.*" upper/mydir/
user.overlay.opaque="y"
위험: trusted.overlay.* xattr을 수동으로 추가/제거/수정하면 OverlayFS 파일시스템이 손상될 수 있습니다. 특히 origin이나 nlink를 잘못 편집하면 파일이 사라지거나 데이터가 불일치합니다. 디버깅 용도의 읽기만 권장합니다.

마운트 옵션 상세

옵션커널설명
lowerdir경로(:경로...)3.18+읽기 전용 레이어 (우측이 최하위, 최대 500개)
upperdir경로3.18+읽기/쓰기 레이어 (생략 시 read-only 마운트)
workdir경로3.18+atomic 연산 임시 디렉토리 (upperdir와 같은 FS)
redirect_diron/off/follow/nofollow4.10+디렉토리 rename redirect 처리 방식
metacopyon/off4.19+메타데이터 전용 copy-up (데이터 지연 복사)
indexon/off4.13+inode index (hardlink copy-up, NFS export 지원)
nfs_exporton/off4.16+NFS file handle 지원 (index=on 필요)
xinoon/off/auto4.17+inode 번호에 레이어 비트 추가 (st_ino 충돌 방지)
volatile(플래그)5.6+sync/fsync 무시 (비정상 종료 시 데이터 손실 위험)
userxattr(플래그)5.11+user.overlay.* 네임스페이스 사용 (rootless용)
uuidon/off/null6.6+레이어 UUID 검증 방식 제어
# 기본 마운트
$ mount -t overlay overlay \
  -o lowerdir=/lower,upperdir=/upper,workdir=/work \
  /merged

# 다중 lower 레이어 (왼쪽이 최상위)
$ mount -t overlay overlay \
  -o lowerdir=/app:/runtime:/base,upperdir=/upper,workdir=/work \
  /merged

# read-only 마운트 (upperdir/workdir 생략)
$ mount -t overlay overlay \
  -o lowerdir=/layer3:/layer2:/layer1 \
  /merged

# CI/CD 최적화: volatile + metacopy
$ mount -t overlay overlay \
  -o lowerdir=/lower,upperdir=/upper,workdir=/work,volatile,metacopy=on \
  /merged

# xino 활성화 (크로스-레이어 inode 번호 일관성)
$ mount -t overlay overlay \
  -o lowerdir=/lower,upperdir=/upper,workdir=/work,xino=on \
  /merged

주요 옵션 상세 설명:

성능 튜닝 및 모범 사례

OverlayFS 성능의 핵심은 copy-up 최소화입니다. 컨테이너 이미지 빌드와 런타임 양쪽에서 최적화할 수 있습니다.

# Bad: 불필요한 copy-up 유발 Dockerfile
FROM ubuntu:22.04
COPY large_dataset.tar.gz /data/           # 레이어 1: 5GB
RUN tar xzf /data/large_dataset.tar.gz     # 레이어 2: 5GB 풀림 + 5GB tar 잔존
RUN rm /data/large_dataset.tar.gz          # 레이어 3: whiteout만 (5GB는 여전히 레이어 1에)
RUN chmod -R 755 /data/extracted/          # 레이어 4: 전체 copy-up 발생!

# Good: copy-up 최소화 Dockerfile
FROM ubuntu:22.04
COPY large_dataset.tar.gz /tmp/
RUN tar xzf /tmp/large_dataset.tar.gz -C /data/ \
    && rm /tmp/large_dataset.tar.gz \
    && chmod -R 755 /data/extracted/          # 단일 레이어에서 모두 처리

Copy-up 최소화 전략:

모니터링 메트릭도구경로/명령해석
upper 디스크 사용량du -shupperdir 경로copy-up 누적량 파악
inode 사용량df -iupper 파일시스템whiteout + copy-up 파일 수
copy-up 이벤트ftraceovl_copy_up_onecopy-up 빈도 추적
overlay 마운트 정보cat/proc/mounts마운트 옵션 확인
레이어 구성mount -ltype overlaylower/upper/work 경로 확인
# ftrace로 copy-up 이벤트 실시간 추적
$ echo 1 > /sys/kernel/debug/tracing/events/overlayfs/ovl_copy_up/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
#           TASK-PID   CPU#  TIMESTAMP  FUNCTION
#              |  |      |       |         |
            bash-1234  [002]  1234.567: ovl_copy_up: path=/etc/config.conf size=4096
            node-5678  [001]  1235.890: ovl_copy_up: path=/app/node_modules/.cache size=52428800

# 추적 중지
$ echo 0 > /sys/kernel/debug/tracing/events/overlayfs/ovl_copy_up/enable
문제원인해결
ENOSPC (공간 부족)copy-up으로 upper 파일시스템 가득불필요한 컨테이너 정리 (docker system prune), upper 볼륨 확장
inode 고갈대량 whiteout/small 파일로 inode 소진df -i로 확인, 이미지 레이어 최적화, mkfs -N으로 inode 수 증가
느린 첫 쓰기대용량 파일 copy-upmetacopy=on 활성화, 볼륨 마운트 사용
stat 불일치cross-layer inode 번호 충돌xino=on 활성화
NFS export 실패file handle 미지원index=on,nfs_export=on 마운트 옵션
rename EXDEVcross-device rename 시도redirect_dir=on 활성화
경고: workdir 디렉토리의 내용을 수동으로 편집하거나 삭제하면 안 됩니다. OverlayFS는 workdir을 atomic 연산(rename, whiteout 생성)의 임시 공간으로 사용하며, 수동 조작 시 진행 중인 연산이 실패하고 파일시스템이 불일치 상태에 빠질 수 있습니다. 마운트 해제 후에도 work/incompat/volatile 등의 내부 파일이 존재할 수 있으며, 이는 정상입니다.

컨테이너 네트워킹

컨테이너 네트워킹은 network namespace로 격리된 네트워크 스택을 veth 페어와 브릿지로 연결하는 구조가 기본입니다.

veth + bridge 모델 (Docker 기본)

Host Network Namespace eth0 (물리 NIC) docker0 bridge (172.17.0.1/16) iptables NAT veth pair veth pair Container 1 (NET NS) eth0 (172.17.0.2) lo (127.0.0.1) default gw: 172.17.0.1 Container 2 (NET NS) eth0 (172.17.0.3) lo (127.0.0.1) default gw: 172.17.0.1
# Docker 브릿지 네트워크 확인
$ ip link show type bridge                    # docker0 확인
$ bridge link show                            # bridge에 연결된 veth 목록
$ ip netns list                               # 네트워크 네임스페이스 목록

# 수동으로 veth + bridge 구성 (교육 목적)
$ ip link add veth0 type veth peer name veth1
$ ip link set veth0 master docker0            # 호스트 측을 bridge에 연결
$ ip link set veth1 netns <container_pid>     # 컨테이너 측을 NS로 이동
$ nsenter -t <pid> -n ip addr add 172.17.0.10/16 dev veth1
$ nsenter -t <pid> -n ip link set veth1 up

CNI (Container Network Interface)

CNI는 CNCF 표준으로, 컨테이너 네트워크 설정을 플러그인 체인으로 구성합니다. Kubernetes, Podman, CRI-O가 CNI를 사용합니다.

// CNI 네트워크 설정 예시 (/etc/cni/net.d/10-bridge.conflist)
{
  "cniVersion": "1.0.0",
  "name": "mynet",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "cni0",
      "isGateway": true,
      "ipam": {
        "type": "host-local",
        "subnet": "10.22.0.0/16",
        "routes": [{ "dst": "0.0.0.0/0" }]
      }
    },
    { "type": "loopback" },
    {
      "type": "portmap",
      "capabilities": { "portMappings": true }
    }
  ]
}

macvlan / ipvlan 모드

모드동작사용 사례
macvlan bridge각 컨테이너에 고유 MAC, 물리 NIC 서브인터페이스외부 네트워크 직접 연결
ipvlan L2MAC 공유, L2 수준 분리MAC 수 제한 환경
ipvlan L3MAC 공유, L3 라우팅 기반 분리대규모 컨테이너 환경
네트워크 관련 참조: 네트워크 네임스페이스와 라우팅 격리 상세는 네임스페이스, iptables/nftables NAT 규칙은 NATNetfilter 페이지를 참조하세요.

Rootless 컨테이너

Rootless 컨테이너는 User namespace를 활용하여 일반 사용자(non-root)가 컨테이너를 실행하는 기술입니다. 호스트의 root 권한 없이 컨테이너 내부에서 "가짜 root"로 동작합니다.

UID/GID 매핑

# /etc/subuid — 사용자별 subordinate UID 범위
user1:100000:65536       # user1에게 UID 100000-165535 할당

# /etc/subgid — 사용자별 subordinate GID 범위
user1:100000:65536       # user1에게 GID 100000-165535 할당

# 매핑: 컨테이너 내 root(UID 0) → 호스트 user1의 subuid(100000)
# 컨테이너 내 UID 1 → 호스트 100001
# ...
# 컨테이너 내 UID 65535 → 호스트 165535
/* 커널 User namespace UID 매핑 (/proc//uid_map 형식) */
/*   container_uid  host_uid  count */
         0       100000      65536

/* newuidmap/newgidmap 명령으로 설정 */
$ newuidmap <pid> 0 100000 65536
$ newgidmap <pid> 0 100000 65536

Rootless 네트워킹

rootless 환경에서는 veth 페어를 생성할 수 없으므로(CAP_NET_ADMIN 부재), 유저스페이스 네트워크 스택을 사용합니다:

구현방식성능
slirp4netnslibslirp 기반 TAP 디바이스 에뮬레이션보통 (유저스페이스 TCP/IP 스택)
pasta호스트와 NS 간 패킷 복사 (splice)빠름 (Linux 5.7+)
RootlessKitslirp4netns/pasta 래퍼구현에 따라 다름

Rootless OverlayFS

Linux 5.11 이전에는 OverlayFS 마운트에 CAP_SYS_ADMIN이 필요했습니다. 커널 5.11부터 User namespace 내부에서 OverlayFS 마운트가 가능해져, FUSE 기반 fuse-overlayfs 없이도 rootless 컨테이너의 이미지 레이어를 직접 사용할 수 있습니다.

네이티브 rootless OverlayFS는 userxattr 마운트 옵션을 자동으로 사용합니다. 이 옵션은 trusted.overlay.* 대신 user.overlay.* 네임스페이스에 메타데이터를 저장하여, 권한 없는 사용자도 xattr을 관리할 수 있게 합니다.

# unshare로 User namespace + Mount namespace 생성 후 OverlayFS 마운트
$ unshare -Urm
# (이제 User NS 내부에서 root 권한)
$ mkdir -p /tmp/lower /tmp/upper /tmp/work /tmp/merged
$ mount -t overlay overlay \
  -o lowerdir=/tmp/lower,upperdir=/tmp/upper,workdir=/tmp/work,userxattr \
  /tmp/merged

# 커널 5.11+ 확인
$ uname -r
5.11.0-generic
$ cat /proc/mounts | grep overlay
overlay /tmp/merged overlay rw,lowerdir=/tmp/lower,upperdir=/tmp/upper,workdir=/tmp/work,userxattr 0 0

커널 5.11 미만이거나 특수한 파일시스템(NFS, FUSE 자체)에서는 fuse-overlayfs가 폴백으로 사용됩니다:

구현커널 요구성능 (상대)제약
Native OverlayFS5.11+100% (커널 레벨)upperdir/lowerdir가 같은 FS 타입 권장
fuse-overlayfs4.18+ (FUSE)~60-80%유저스페이스 오버헤드, context switch 비용

Podman과 Buildah는 런타임에 커널 버전을 감지하여 자동으로 적합한 드라이버를 선택합니다:

# Podman 스토리지 드라이버 확인
$ podman info --format '{{.Store.GraphDriverName}}'
overlay     # 커널 5.11+ → native overlay

# 설정 파일에서 직접 제어 (~/.config/containers/storage.conf)
[storage]
driver = "overlay"
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"   # 강제 FUSE 사용 시
자동 감지: Podman 4.0+는 /proc/sys/kernel/unprivileged_userns_clone과 실제 mount(2) 시도를 통해 native overlay 지원 여부를 판별합니다. 별도 설정 없이 최적의 드라이버가 자동 선택되므로, 대부분의 경우 수동 설정은 불필요합니다.

Rootless 제한사항

컨테이너 보안 심화

보안 계층 종합

계층기술보호 대상
프로세스 격리NamespacesPID/네트워크/마운트/사용자 가시성
리소스 제한cgroupsCPU, 메모리, I/O, PID 수
시스템 콜 필터seccomp-bpf커널 공격 표면 축소
권한 최소화Capabilitiesroot 권한 세분화
MACAppArmor / SELinux파일/네트워크/프로세스 접근 제어
읽기 전용 rootfsOverlayFS + readonly파일시스템 무결성
No New PrivilegesPR_SET_NO_NEW_PRIVSsetuid 바이너리를 통한 권한 상승

AppArmor 컨테이너 프로파일

# Docker 기본 AppArmor 프로파일 (docker-default) 핵심 규칙
profile docker-default flags=(attach_disconnected,mediate_deleted) {
  # 기본적으로 대부분의 파일 접근 허용
  file,
  network,
  capability,

  # 명시적 deny 규칙
  deny mount,                           # 마운트 차단
  deny /sys/firmware/** rwklx,          # 펌웨어 접근 차단
  deny /sys/kernel/security/** rwklx,   # LSM 인터페이스 접근 차단
  deny /proc/sys/** wklx,               # 커널 파라미터 쓰기 차단
  deny /proc/sysrq-trigger rwklx,       # SysRq 차단
  deny /proc/kcore rwklx,               # 커널 메모리 접근 차단
}

SELinux 컨테이너 정책

SELinux 타입설명
container_t일반 컨테이너 프로세스 타입
container_file_t컨테이너 파일시스템 타입
container_var_lib_t/var/lib/containers 하위
spc_t슈퍼 특권 컨테이너 (--privileged)
# SELinux 컨테이너 컨텍스트 확인
$ ps -eZ | grep container
system_u:system_r:container_t:s0:c123,c456  ... nginx

# 볼륨 마운트 시 레이블 지정
$ podman run -v /data:/data:Z alpine      # :Z = private 레이블 재지정
$ podman run -v /data:/data:z alpine      # :z = shared 레이블 재지정

컨테이너 탈출 공격 벡터와 방어

공격 벡터설명방어
/proc, /sys 남용호스트 정보 유출, 커널 파라미터 조작read-only bind mount, masked paths
CAP_SYS_ADMINmount, BPF, namespace 생성 등 광범위 권한--cap-drop=ALL, 필요 cap만 추가
커널 취약점CVE를 통한 호스트 커널 직접 공격seccomp 필터, 커널 업데이트, gVisor/Kata
Docker 소켓 마운트/var/run/docker.sock으로 호스트 컨테이너 제어소켓 마운트 금지, rootless 사용
--privileged 플래그모든 보안 장치 비활성화절대 사용 금지, 대안 capability 사용

컨테이너 보안 주요 CVE 사례

컨테이너 탈출과 권한 상승 취약점의 실제 사례를 분석합니다. 컨테이너 런타임, OverlayFS, cgroup 등 다양한 계층에서 발견된 취약점들이 컨테이너 보안 아키텍처의 발전을 이끌어왔습니다.

CVE-2019-5736 — runc 컨테이너 탈출 (CVSS 8.6):

runc의 /proc/self/exe 처리에서 컨테이너 내부의 악성 프로세스가 호스트의 runc 바이너리를 덮어쓸 수 있는 취약점입니다. 컨테이너가 runc exec을 통해 진입될 때, 컨테이너 내 악성 /bin/sh/proc/self/exe 심볼릭 링크를 통해 호스트의 runc 바이너리에 접근하여 덮어씁니다. 이후 호스트에서 runc가 실행될 때마다 공격자의 코드가 root 권한으로 실행됩니다.

/* CVE-2019-5736: runc /proc/self/exe 악용 흐름 */

/*
 * 공격 조건: 컨테이너 내부에서 코드 실행 가능 (이미지 포이즌 또는 RCE)
 *
 * 1. 컨테이너 entrypoint를 악성 바이너리로 교체
 * 2. 관리자가 "docker exec  /bin/sh" 실행
 * 3. runc가 컨테이너의 네임스페이스에 진입
 *    → /proc/self/exe가 호스트의 runc 바이너리를 가리킴
 * 4. 악성 프로세스가 /proc/self/exe를 O_WRONLY로 열기 시도
 *    → runc 바이너리가 실행 중이므로 ETXTBSY 반환
 * 5. runc가 execve()로 /bin/sh를 실행한 후,
 *    /proc//exe → 호스트 runc 바이너리
 *    → 이제 ETXTBSY 없이 쓰기 가능
 * 6. 호스트의 runc 바이너리가 악성 코드로 덮어써짐
 *
 * 수정: runc가 자신의 바이너리를 memfd_create()로
 *       메모리에 복사 후 실행 (호스트 파일 직접 참조 방지)
 */
CVE-2022-0492 — cgroup v1 release_agent 컨테이너 탈출:

cgroup v1의 release_agent 파일을 통해 컨테이너 내부에서 호스트의 root 권한으로 임의 명령을 실행할 수 있습니다. 컨테이너가 CAP_SYS_ADMIN + cgroup 마운트 권한을 가진 경우, release_agent에 호스트 경로의 스크립트를 지정하고 cgroup을 비우면 해당 스크립트가 호스트에서 실행됩니다.

/* CVE-2022-0492 공격 흐름 (CAP_SYS_ADMIN 필요) */

# 1. 컨테이너 내부에서 cgroup v1 마운트
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp

# 2. release_agent에 호스트에서 실행할 명령 지정
echo /path/on/host/exploit.sh > /tmp/cgrp/release_agent

# 3. notify_on_release 활성화
mkdir /tmp/cgrp/x && echo 1 > /tmp/cgrp/x/notify_on_release

# 4. cgroup을 비우면 release_agent가 호스트에서 실행됨
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs" && sleep 1

/*
 * 방어:
 * 1. --cap-drop=ALL (CAP_SYS_ADMIN 제거)
 * 2. seccomp 프로파일에서 mount, unshare 시스콜 차단
 * 3. AppArmor/SELinux로 cgroupfs 마운트 거부
 * 4. cgroup v2 사용 (release_agent 메커니즘 없음)
 * 5. 커널 5.17+에서 cgroup namespace 내 release_agent 쓰기 차단
 */
CVE-2023-0386 — OverlayFS setuid 권한 상승:

OverlayFS의 lower layer에 FUSE 파일시스템을 마운트하면, FUSE에서 setuid 비트가 설정된 파일을 제공할 수 있습니다. OverlayFS가 이 파일을 upper layer로 copy-up할 때 setuid 비트가 유지되어, 일반 사용자가 user namespace 내에서 root 소유의 setuid 바이너리를 생성할 수 있습니다.

CVE-2024-21626 — runc 작업 디렉터리 컨테이너 탈출:

runc 1.1.11 이전 버전에서 WORKDIR 지시문이 /proc/self/fd/<fd>와 같은 경로로 설정된 경우, 내부 파일 디스크립터 누수를 통해 호스트 파일시스템에 접근할 수 있습니다. 컨테이너 이미지 빌드 시 또는 runc exec --cwd 실행 시 트리거됩니다.

/* CVE-2024-21626: 파일 디스크립터 누수를 통한 탈출 */

/*
 * runc가 컨테이너 초기화 중 일부 fd를 닫지 않아
 * 호스트 파일시스템의 fd가 컨테이너 내부로 누수
 *
 * 공격: WORKDIR=/proc/self/fd/7 (호스트 / 를 가리키는 fd)
 * → 컨테이너 내부에서 호스트 루트 파일시스템 접근 가능
 *
 * 수정: runc 초기화 시 불필요한 fd를 O_CLOEXEC으로 닫고,
 *       /proc/self/fd 순회하여 예상치 못한 fd 정리
 */

/* 컨테이너 보안 점검 체크리스트 */
# 1. 런타임 버전 확인
$ runc --version   # 1.1.12+ 필수
$ containerd --version

# 2. 컨테이너 capability 확인
$ docker inspect --format '{{.HostConfig.CapAdd}}' <container>
# SYS_ADMIN, SYS_PTRACE, NET_ADMIN 등 불필요 cap 제거

# 3. seccomp 프로파일 확인
$ docker inspect --format '{{.HostConfig.SecurityOpt}}' <container>

# 4. AppArmor/SELinux 상태 확인
$ aa-status   # AppArmor
$ getenforce  # SELinux
컨테이너 보안 CVE 타임라인과 교훈:

2019: CVE-2019-5736 (runc 탈출) → memfd 기반 self-clone 도입
2020: CVE-2020-15257 (containerd shim) → abstract socket → filesystem socket 전환
2022: CVE-2022-0185 (fsconfig 힙 오버플로우), CVE-2022-0492 (cgroup v1 release_agent) → cgroup v2 전환 가속화
2023: CVE-2023-0386 (OverlayFS setuid) → OverlayFS inode 검증 강화
2024: CVE-2024-21626 (runc fd 누수) → fd 정리 로직 강화
핵심 교훈: 컨테이너 격리의 각 계층(런타임, 커널 서브시스템, 파일시스템)에서 독립적인 취약점이 발견될 수 있으므로, 심층 방어(defense-in-depth) 원칙 적용이 필수적입니다.

gVisor / Kata Containers: 강화 격리

기본 컨테이너는 호스트 커널을 공유하므로 커널 취약점에 노출됩니다. 이를 해결하는 두 가지 접근법이 있습니다:

기술접근법오버헤드호환성
gVisor (runsc)유저스페이스 커널 — syscall interception중간 (syscall 오버헤드)일부 syscall 미구현
Kata Containers경량 VM 내부에서 컨테이너 실행높음 (VM 부팅 오버헤드)매우 높음

Kata Containers 심화

Kata Containers는 경량 가상 머신(VM) 내부에서 컨테이너 워크로드를 실행하여, VM 수준의 격리와 컨테이너 수준의 사용자 경험을 결합합니다.

Host (containerd + Kata shimv2) Lightweight VM (QEMU / Cloud Hypervisor) Guest Linux Kernel (경량 커널, ~100ms 부팅) kata-agent (gRPC) virtiofs (파일시스템 공유) virtio-net (네트워킹) Container 1 rootfs (virtiofs) Application Container 2 rootfs (virtiofs) Application Kata 특성 VM 부팅: ~100-200ms 메모리: ~20-40MB (게스트 커널) hypervisor: QEMU / CLH / FC 격리: VM 수준 (hw virt)

containerd shimv2 연동

Kata는 containerd의 shimv2 인터페이스를 구현하여, 기존 containerd 워크플로에 투명하게 통합됩니다.

# containerd 설정에서 Kata 런타임 등록
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
  runtime_type = "io.containerd.kata.v2"

# Kubernetes에서 RuntimeClass로 사용
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata
handler: kata

# Pod에서 Kata 런타임 지정
apiVersion: v1
kind: Pod
spec:
  runtimeClassName: kata
  containers:
  - name: app
    image: nginx

주요 설정 옵션 (configuration.toml)

옵션설명기본값
hypervisorQEMU, Cloud Hypervisor, FirecrackerQEMU
default_memoryVM 메모리 (MB)256
default_vcpusVM vCPU 수1
shared_fs파일시스템 공유 방식virtiofs
kernel_path게스트 커널 경로/usr/share/kata-containers/vmlinux
enable_debug디버그 모드false
virtiofs 상세: Kata가 사용하는 virtiofs 프로토콜의 커널 내부 구현은 FUSE 페이지의 virtiofs 섹션을 참조하세요. KVM 가상화 내부는 가상화 (KVM) 페이지를 참조하세요.

최소 컨테이너 직접 구현

컨테이너의 내부 동작을 이해하기 위해, 셸 스크립트와 커널 API만으로 최소 컨테이너를 직접 구현해 봅니다.

셸 스크립트로 컨테이너 구현 (교육 목적)

#!/bin/bash
# mini-container.sh — 교육용 최소 컨테이너
# root 권한 필요 (rootless는 unshare --user 추가)

set -e
ROOTFS="./rootfs"   # alpine rootfs 등 준비 필요
HOSTNAME="mini-container"

## 1단계: 네임스페이스 생성 + 새 프로세스 시작
unshare --mount --uts --ipc --pid --net --fork \
        --mount-proc=$ROOTFS/proc \
  /bin/bash -c "
    # 2단계: 호스트명 설정 (UTS namespace)
    hostname $HOSTNAME

    # 3단계: 루트 파일시스템 전환 (pivot_root)
    mount --bind $ROOTFS $ROOTFS
    cd $ROOTFS
    mkdir -p .put_old
    pivot_root . .put_old
    umount -l /.put_old
    rmdir /.put_old

    # 4단계: 필수 파일시스템 마운트
    mount -t proc proc /proc
    mount -t sysfs sysfs /sys
    mount -t tmpfs tmpfs /dev

    # 5단계: cgroup 리소스 제한 (v2)
    # (호스트에서 미리 cgroup 디렉토리를 생성해야 함)
    # echo 50000 100000 > /sys/fs/cgroup/mini/cpu.max
    # echo 67108864 > /sys/fs/cgroup/mini/memory.max  # 64MB
    # echo \$\$ > /sys/fs/cgroup/mini/cgroup.procs

    # 6단계: 컨테이너 셸 실행
    exec /bin/sh
  "

runc bundle 생성 및 실행 예제

# OCI Bundle 생성 (rootfs + config.json)
$ mkdir -p mycontainer/rootfs

# Alpine rootfs 준비
$ docker export $(docker create alpine:latest) | \
    tar -C mycontainer/rootfs -xf -

# OCI 기본 config.json 생성
$ cd mycontainer
$ runc spec

# config.json 수정 (예: 메모리 256MB 제한 추가)
# "resources": { "memory": { "limit": 268435456 } }

# 컨테이너 실행
$ runc run my-test-container

# 다른 터미널에서 상태 확인
$ runc list
ID                  PID     STATUS      BUNDLE                CREATED
my-test-container   12345   running     /home/user/mycontainer 2024-...

# 컨테이너 내부에서 격리 확인
/ # hostname
runc
/ # ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 sh
    7 root      0:00 ps aux
/ # cat /proc/1/cgroup
0::/

디버깅과 모니터링

네임스페이스 진입 및 확인

# nsenter — 실행 중인 컨테이너의 네임스페이스에 진입
$ nsenter -t <PID> --mount --uts --ipc --net --pid -- /bin/sh
#   -t: 대상 프로세스 PID
#   각 플래그로 원하는 네임스페이스만 선택적으로 진입 가능

# lsns — 시스템의 모든 네임스페이스 나열
$ lsns
        NS TYPE   NPROCS   PID USER   COMMAND
4026531836 pid         1  1234 root   nginx: master process
4026531840 mnt         1  1234 root   nginx: master process
4026531992 net         1  1234 root   nginx: master process

# 특정 프로세스의 네임스페이스 ID 확인
$ ls -la /proc/<PID>/ns/
lrwxrwxrwx 1 root root 0  ... cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0  ... mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0  ... net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0  ... pid -> 'pid:[4026531836]'

프로세스 네임스페이스 정보

# /proc//status에서 네임스페이스별 PID 확인
$ grep NS /proc/<PID>/status
NSpid:  1234    1              # 호스트 PID 1234, 컨테이너 내 PID 1
NStgid: 1234    1
NSsid:  1234    1
NSpgid: 1234    1

# 컨테이너 프로세스의 cgroup 경로
$ cat /proc/<PID>/cgroup
0::/system.slice/docker-abc123.scope

cgroup 모니터링

# systemd-cgtop — cgroup별 리소스 사용량 실시간 모니터링
$ systemd-cgtop
Control Group            Tasks   %CPU  Memory  Input/s  Output/s
/                           85   12.3   1.2G        -        -
/system.slice/docker-abc..   3    5.1  128.0M    4.0K    12.0K

# cgroup v2 직접 확인
$ cat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.current
134217728      # 128MB 사용 중
$ cat /sys/fs/cgroup/system.slice/docker-abc123.scope/cpu.stat
usage_usec 5234567
user_usec 3123456
system_usec 2111111
$ cat /sys/fs/cgroup/system.slice/docker-abc123.scope/pids.current
3              # 3개 프로세스

디버깅 도구 비교

도구대상 런타임용도
docker inspectDocker컨테이너 상세 정보 (설정, 네트워크, 마운트)
crictlCRI 런타임Kubernetes 노드의 컨테이너/파드 디버깅
ctrcontainerdcontainerd 직접 제어 (이미지, 컨테이너, 태스크)
nerdctlcontainerdDocker-호환 CLI (compose 지원)
podman inspectPodmanDocker와 동일한 inspect 인터페이스
runc list/stateruncOCI 런타임 수준 상태 확인
# crictl — Kubernetes 노드 디버깅
$ crictl pods                                # 파드 목록
$ crictl ps                                  # 컨테이너 목록
$ crictl inspect <container-id>              # 컨테이너 상세
$ crictl logs <container-id>                 # 로그 확인
$ crictl exec -it <container-id> /bin/sh     # 컨테이너 내부 셸

일반적인 문제

문제증상해결
OOM Kill컨테이너 갑자기 종료, exit code 137memory.max 확인, dmesg에서 OOM 로그, 메모리 제한 조정
PID 1 좀비좀비 프로세스 누적, PID 고갈init 프로세스 사용 (tini, dumb-init), --init 플래그
마운트 누수컨테이너 삭제 후 마운트 포인트 잔존MNT_DETACH로 lazy unmount, findmnt로 확인
DNS 실패컨테이너에서 이름 해석 불가/etc/resolv.conf 확인, --dns 옵션, bridge 설정 검증
퍼미션 에러rootless에서 파일 접근 거부subuid/subgid 매핑 확인, 볼륨 소유권 조정