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

🎯 이 글에서 다루는 것
- 도커(컨테이너)가 격리를 구현하는 리눅스 커널 메커니즘 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
보안이 중요한 워크로드라면 다음을 고려하세요.
- Rootless 컨테이너 또는 User Namespace 활성화
- Seccomp / AppArmor / SELinux 프로파일 적용
- --privileged 옵션 사용 금지
- 민감한 워크로드는 gVisor 또는 Kata Containers 같은 강화된 런타임 검토
'클라우드' 카테고리의 다른 글
| AWS와 Terraform으로 인프라를 코드로 관리하는 법 🏗️ (0) | 2026.04.03 |
|---|---|
| 아마존 Q로 할 수 있는 모든 것 — AWS가 만든 AI 어시스턴트의 진짜 능력 🤖 (0) | 2026.04.02 |
| 🍀 HashiCorp가 배신했다? — Terraform의 역사와 OpenTofu가 탄생한 진짜 이유 (0) | 2026.03.28 |
| 🥊 AWS Image Builder vs Packer — 실무 DevOps 엔지니어라면 뭘 써야 할까? (0) | 2026.03.27 |
| 🏗️ 개발자라면 인프라 배포를 알아야 하는 진짜 이유 — "코드만 잘 짜면 된다"는 착각 (0) | 2026.03.27 |