본문 바로가기
클라우드

🐳 도커는 진짜로 무엇을 격리하는가? — 커널 기능으로 보는 컨테이너의 속살

by gasbugs 2026. 3. 30.

컨테이너는 가상머신이 아니다. 그런데도 격리된 것처럼 느껴진다. 왜일까?

 

 

🎯 이 글에서 다루는 것

  • 도커(컨테이너)가 격리를 구현하는 리눅스 커널 메커니즘 2가지
  • Namespace — 무엇이 보이는지를 격리
  • cgroups — 얼마나 쓸 수 있는지를 제한
  • 격리되지 않는 것들 — 보안 설계 시 반드시 알아야 할 함정
  • 가상머신(VM)과 컨테이너의 격리 수준 비교

📌 도입 / 배경

도커를 처음 배울 때 가장 많이 듣는 말이 있습니다.

컨테이너는 격리된 환경이에요.

맞습니다. 그런데 어디까지 격리되는 걸까요?

실제로 많은 개발자와 운영자들이 컨테이너를 VM처럼 완전히 독립된 환경으로 오해합니다. 그 결과 컨테이너 탈출(Container Escape) 취약점에 무방비 상태가 되거나, 한 컨테이너가 호스트 전체 CPU를 독식하는 상황을 맞닥뜨리기도 하죠.

도커의 격리를 제대로 이해하려면 리눅스 커널 수준에서 어떤 일이 벌어지는지를 알아야 합니다. 오늘은 그 속살을 파헤쳐 보겠습니다.


🔍 1. Namespace — "무엇이 보이는지"를 격리한다

리눅스 Namespace는 프로세스가 볼 수 있는 시스템 자원의 범위를 제한하는 커널 기능입니다. 같은 호스트 위에 있지만, 컨테이너 안의 프로세스는 마치 자기만의 세계가 있는 것처럼 느끼게 됩니다.

도커가 사용하는 Namespace는 총 7가지입니다.

 

🗂️ PID Namespace — 프로세스 ID 격리

컨테이너 안에서 ps aux를 실행하면 자기 프로세스만 보입니다. 호스트에서 실행 중인 수백 개의 프로세스는 보이지 않죠.

컨테이너 안의 첫 번째 프로세스는 항상 PID 1로 표시됩니다. 하지만 호스트에서 보면 완전히 다른 PID 번호를 가지고 있습니다.

# 호스트에서 확인
$ docker run --rm ubuntu sleep 1000 &
$ ps aux | grep sleep
root  12345  ...  sleep 1000   # 호스트 PID: 12345

# 컨테이너 안에서 확인
$ docker exec <container_id> ps aux
PID  USER  COMMAND
1    root  sleep 1000          # 컨테이너 안에서는 PID 1

 

🌐 Network Namespace — 네트워크 격리

각 컨테이너는 자신만의 가상 네트워크 인터페이스, IP 주소, 라우팅 테이블을 가집니다. 컨테이너 A에서 ifconfig를 실행하면 컨테이너 B의 네트워크 인터페이스는 보이지 않습니다.

도커는 기본적으로 docker0라는 브리지 네트워크를 생성하고, 각 컨테이너를 가상 이더넷 페어(veth pair)로 연결합니다.

 

📁 Mount Namespace — 파일시스템 격리

컨테이너는 자신만의 루트 파일시스템(/)을 가집니다. 이것이 바로 Ubuntu 이미지를 실행하면 apt가 동작하고, Alpine 이미지를 실행하면 apk가 동작하는 이유입니다.

호스트의 파일시스템은 기본적으로 보이지 않습니다. -v 옵션으로 명시적으로 마운트하지 않는 한.

 

👤 UTS Namespace — 호스트명 격리

컨테이너는 자신만의 호스트명(hostname)과 도메인명을 가집니다. 컨테이너 안에서 hostname을 실행하면 호스트의 이름이 아닌 컨테이너 ID가 나타납니다.

 

🔐 IPC Namespace — 프로세스 간 통신 격리

System V IPC(공유 메모리, 세마포어, 메시지 큐)를 격리합니다. 컨테이너 A의 프로세스가 컨테이너 B의 공유 메모리에 접근하는 것을 차단합니다.

 

👥 User Namespace — 사용자 ID 격리

컨테이너 안에서 root(UID 0)인 사용자가 호스트에서는 일반 사용자로 매핑될 수 있습니다. 이 기능은 Rootless 컨테이너를 가능하게 하는 핵심이지만, 도커의 기본 설정에서는 활성화되어 있지 않습니다.

 

🔗 Cgroup Namespace — cgroup 뷰 격리

컨테이너가 자신의 cgroup 계층만 볼 수 있도록 제한합니다. (cgroup 자체에 대한 설명은 바로 아래에서 다룹니다.)


🔍 2. cgroups — "얼마나 쓸 수 있는지"를 제한한다

cgroups(Control Groups)는 프로세스 그룹이 사용할 수 있는 시스템 자원의 양을 제한, 측정, 격리하는 커널 기능입니다.

Namespace가 "무엇이 보이는가"를 다룬다면, cgroups는 "얼마나 쓸 수 있는가"를 다룹니다.

도커가 제어하는 주요 자원은 다음과 같습니다.

자원 옵션 예시 설명
CPU --cpus="1.5" 최대 1.5개 코어 사용
메모리 --memory="512m" 최대 512MB RAM
디스크 I/O --blkio-weight=500 블록 I/O 가중치 설정
네트워크 (tc 연동) 대역폭 제한
 
# CPU 1개, 메모리 512MB로 제한된 컨테이너 실행
docker run --cpus="1.0" --memory="512m" nginx

# 실제 cgroup 파일 확인 (호스트에서)
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes
# 536870912 (= 512MB)

cgroups가 없다면 어떤 일이 벌어질까요? 악의적이거나 버그가 있는 컨테이너 하나가 호스트의 CPU를 100% 점유하거나 메모리를 모두 소진해서 전체 시스템을 다운시킬 수 있습니다. 이것을 Noisy Neighbor 문제라고 부릅니다.


⚠️ 격리되지 않는 것들 — 여기서 보안 구멍이 생긴다

이제 가장 중요한 부분입니다. 도커 컨테이너가 격리하지 못하는 것들입니다. 이것을 모르면 클라우드 인프라 설계나 보안 감사에서 치명적인 실수를 합니다.

❌ 커널 자체는 공유된다

이것이 컨테이너와 VM의 가장 큰 차이점입니다.

[VM]                          [컨테이너]
┌─────────────────────┐       ┌───────────────────────────┐
│  Guest OS Kernel A  │       │  Container A  Container B │
├─────────────────────┤       │  (Ubuntu)     (Alpine)    │
│  Guest OS Kernel B  │       ├───────────────────────────┤
├─────────────────────┤       │   ← 커널 공유 →           │
│   Hypervisor        │       │   Host Linux Kernel       │
├─────────────────────┤       ├───────────────────────────┤
│   Host OS Kernel    │       │   Host Hardware           │
└─────────────────────┘       └───────────────────────────┘

 

컨테이너는 호스트의 리눅스 커널을 그대로 사용합니다. 따라서 컨테이너 안의 프로세스가 커널 취약점을 익스플로잇하면 호스트 전체가 위험에 노출됩니다. 이것이 컨테이너 탈출(Container Escape) 공격의 원리입니다.

 

 

❌ 시스템 콜(System Call)은 직접 호스트 커널로 전달된다

컨테이너 안의 프로세스가 read(), write(), socket() 같은 시스템 콜을 호출하면, 이 요청은 중간 계층 없이 호스트 커널로 직접 전달됩니다. VM처럼 하이퍼바이저가 가로채서 검증하는 레이어가 없습니다.

이 때문에 도커는 Seccomp(Secure Computing Mode) 프로파일을 기본 적용하여 위험한 시스템 콜을 차단합니다. 하지만 모든 시스템 콜을 막을 수는 없습니다.

# 기본 Seccomp 프로파일 확인
docker inspect <container_id> | grep -i seccomp

 

❌ /proc, /sys 일부는 공유된다

--pid=host 옵션처럼 실수로 네임스페이스를 호스트와 공유하면, /proc 파일시스템을 통해 호스트의 프로세스 정보가 노출됩니다.

 

❌ User Namespace는 기본 비활성화

앞서 언급한 User Namespace는 도커의 기본 설정에서 비활성화되어 있습니다. 즉, 컨테이너 안의 root는 호스트에서도 root입니다. 볼륨 마운트와 결합되면 호스트 파일에 대한 root 접근이 가능해질 수 있습니다.

# 위험한 패턴 — 호스트 루트 파일시스템을 컨테이너에 마운트
docker run -v /:/host --rm -it ubuntu chroot /host
# 이러면 컨테이너 안에서 호스트 전체에 접근 가능

 

💻 실습: Namespace 직접 확인하기

# 컨테이너의 Namespace 목록 확인 (호스트에서)
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' <container_id>)
ls -la /proc/$CONTAINER_PID/ns/

# 출력 예시
lrwxrwxrwx 1 root root 0 ... cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 ... ipc    -> ipc:[4026532456]
lrwxrwxrwx 1 root root 0 ... mnt    -> mnt:[4026532454]
lrwxrwxrwx 1 root root 0 ... net    -> net:[4026532459]
lrwxrwxrwx 1 root root 0 ... pid    -> pid:[4026532457]
lrwxrwxrwx 1 root root 0 ... uts    -> uts:[4026532455]
# user Namespace가 호스트와 동일하다면? → User Namespace 미사용 상태
 
# 호스트와 컨테이너의 user namespace 비교
ls -la /proc/1/ns/user        # 호스트 init 프로세스
ls -la /proc/$CONTAINER_PID/ns/user  # 컨테이너 프로세스
# 심볼릭 링크의 숫자가 같으면 → 같은 User Namespace = 위험

✅ 정리 / 마무리

구분 기능 격리 대상
Namespace PID, Network, Mount, UTS, IPC, User, Cgroup 자원의 가시성
cgroups CPU, 메모리, I/O, 네트워크 자원의 사용량
격리 안 됨 커널, 시스템 콜, User NS(기본) 보안 경계의 한계

도커의 격리는 강력하지만 완전하지 않습니다. 핵심을 정리하면 이렇습니다.

  • 🟢 Namespace: 프로세스, 네트워크, 파일시스템 등 자원의 가시성을 격리
  • 🟢 cgroups: CPU, 메모리 등 자원의 사용량을 제한
  • 🔴 커널은 공유: 컨테이너는 같은 리눅스 커널을 사용하며, 이것이 VM과의 근본적 차이
  • 🔴 User Namespace 기본 비활성화: 컨테이너 root = 호스트 root

 

보안이 중요한 워크로드라면 다음을 고려하세요.

  1. Rootless 컨테이너 또는 User Namespace 활성화
  2. Seccomp / AppArmor / SELinux 프로파일 적용
  3. --privileged 옵션 사용 금지
  4. 민감한 워크로드는 gVisor 또는 Kata Containers 같은 강화된 런타임 검토