Linux Namespace란?
Linux Namespace는 커널 수준에서 프로세스의 시스템 리소스 뷰를 격리하는 메커니즘입니다. Docker, Kubernetes, containerd 등 모든 컨테이너 런타임은 내부적으로 Namespace를 사용하여 프로세스를 격리합니다. Namespace를 이해하면 컨테이너가 “어떻게” 격리되는지 근본 원리를 파악할 수 있습니다.
이 글에서는 8가지 Namespace의 동작 원리, 실전 생성·확인 방법, 컨테이너 런타임과의 관계, 그리고 보안 관점까지 심층적으로 다룹니다. Linux cgroup v2 리소스 제어 심화와 함께 읽으면 컨테이너 격리의 두 축(Namespace = 가시성 격리, cgroup = 자원량 제한)을 모두 이해할 수 있습니다.
8가지 Namespace 종류
| Namespace | 격리 대상 | 플래그 | 도입 버전 |
|---|---|---|---|
| Mount (mnt) | 파일시스템 마운트 포인트 | CLONE_NEWNS | 2.4.19 |
| UTS | 호스트명, 도메인명 | CLONE_NEWUTS | 2.6.19 |
| IPC | System V IPC, POSIX 메시지 큐 | CLONE_NEWIPC | 2.6.19 |
| PID | 프로세스 ID | CLONE_NEWPID | 2.6.24 |
| Network (net) | 네트워크 스택 (인터페이스, 라우팅, iptables) | CLONE_NEWNET | 2.6.29 |
| User | UID/GID 매핑 | CLONE_NEWUSER | 3.8 |
| Cgroup | cgroup 루트 디렉토리 뷰 | CLONE_NEWCGROUP | 4.6 |
| Time | 시스템 시계 (CLOCK_MONOTONIC, CLOCK_BOOTTIME) | CLONE_NEWTIME | 5.6 |
Namespace 확인과 조작
# 현재 프로세스의 Namespace 확인
ls -la /proc/self/ns/
# lrwxrwxrwx 1 root root 0 cgroup -> 'cgroup:[4026531835]'
# lrwxrwxrwx 1 root root 0 ipc -> 'ipc:[4026531839]'
# lrwxrwxrwx 1 root root 0 mnt -> 'mnt:[4026531841]'
# lrwxrwxrwx 1 root root 0 net -> 'net:[4026531840]'
# lrwxrwxrwx 1 root root 0 pid -> 'pid:[4026531836]'
# lrwxrwxrwx 1 root root 0 user -> 'user:[4026531837]'
# lrwxrwxrwx 1 root root 0 uts -> 'uts:[4026531838]'
# 숫자(inode)가 같으면 같은 Namespace에 속함
# 컨테이너와 호스트의 Namespace 비교
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' mycontainer)
ls -la /proc/$CONTAINER_PID/ns/
ls -la /proc/1/ns/
# → net, pid, mnt 등의 inode가 다르면 격리된 것
# 특정 Namespace에 진입 (nsenter)
nsenter --target $CONTAINER_PID --mount --uts --ipc --net --pid -- /bin/bash
# → 컨테이너 내부와 동일한 뷰로 진입
# lsns: 시스템의 모든 Namespace 목록
lsns --type=net
# NS TYPE NPROCS PID USER COMMAND
# 4026531840 net 150 1 root /sbin/init
# 4026532200 net 5 1234 root nginx: master
PID Namespace 심화
PID Namespace는 프로세스 ID를 격리하여, 컨테이너 내부에서 PID 1(init)부터 시작하는 독립된 프로세스 트리를 만듭니다.
# unshare로 새 PID Namespace 생성
unshare --pid --fork --mount-proc /bin/bash
# Namespace 내부에서:
ps aux
# PID USER COMMAND
# 1 root /bin/bash ← 이 프로세스가 PID 1!
# 2 root ps aux
echo $$
# 1
# 호스트에서 동일 프로세스 확인:
# PID 1이 아니라 큰 번호 (예: 45678)로 보임
# → PID Namespace는 "뷰"를 바꾸는 것, 실제 프로세스는 하나
# PID 1의 특수성:
# - SIGKILL, SIGSTOP이 무시됨 (핸들러 설정한 시그널만 수신)
# - PID 1이 죽으면 Namespace 내 모든 프로세스가 SIGKILL 받음
# - 좀비 프로세스 리핑(reaping) 책임
# Kubernetes shareProcessNamespace
# Pod 내 컨테이너들이 PID Namespace를 공유
apiVersion: v1
kind: Pod
spec:
shareProcessNamespace: true # PID Namespace 공유
containers:
- name: app
image: myapp
- name: sidecar
image: debug-tools
# sidecar에서 app의 프로세스를 ps로 볼 수 있음
# kill 명령으로 app 프로세스에 시그널 전송 가능
Network Namespace 심화
Network Namespace는 네트워크 스택 전체(인터페이스, 라우팅 테이블, iptables, 소켓)를 격리합니다. Docker와 Kubernetes 네트워킹의 기초입니다.
# 새 Network Namespace 생성
ip netns add myns
# Namespace 내부에서 인터페이스 확인
ip netns exec myns ip link
# 1: lo: <LOOPBACK> ... state DOWN
# → 격리된 네트워크: lo만 있고 DOWN 상태
# veth 페어로 호스트와 연결 (Docker 브리지 원리)
# 1. veth 페어 생성
ip link add veth-host type veth peer name veth-ns
# 2. 한쪽을 Namespace에 넣기
ip link set veth-ns netns myns
# 3. IP 할당
ip addr add 10.200.0.1/24 dev veth-host
ip link set veth-host up
ip netns exec myns ip addr add 10.200.0.2/24 dev veth-ns
ip netns exec myns ip link set veth-ns up
ip netns exec myns ip link set lo up
# 4. 통신 확인
ip netns exec myns ping 10.200.0.1
# → 성공! veth 페어를 통해 호스트와 통신
# Docker 컨테이너 네트워크 구조:
# [Container A: veth-a (172.17.0.2)] ──veth── [docker0 브리지 (172.17.0.1)] ── [eth0 (호스트)]
# [Container B: veth-b (172.17.0.3)] ──veth──┘
# Kubernetes Pod 네트워크 구조:
# Pod 내 컨테이너들은 같은 Network Namespace 공유
# → localhost로 서로 통신 가능 (포트 충돌 주의!)
# Pod 간 통신은 CNI 플러그인이 처리 (Calico, Cilium 등)
Mount Namespace 심화
Mount Namespace는 파일시스템 마운트 포인트를 격리합니다. 컨테이너가 자신만의 루트 파일시스템을 갖는 원리입니다.
# 새 Mount Namespace에서 tmpfs 마운트
unshare --mount /bin/bash
mount -t tmpfs tmpfs /mnt
echo "secret" > /mnt/data.txt
# 다른 터미널(호스트)에서:
ls /mnt/
# → 비어있음! Mount Namespace가 격리
# 컨테이너 rootfs 원리 (pivot_root)
# 1. 새 Mount Namespace 생성
# 2. 컨테이너 이미지를 overlayfs로 마운트
# 3. pivot_root로 루트 변경
# 4. 이전 루트 umount
# overlayfs 구조 (Docker 기본 스토리지 드라이버)
mount -t overlay overlay
-o lowerdir=/var/lib/docker/overlay2/layer1:/var/lib/docker/overlay2/layer2,
upperdir=/var/lib/docker/overlay2/container/diff,
workdir=/var/lib/docker/overlay2/container/work
/merged
# lowerdir: 읽기전용 이미지 레이어 (아래→위 순서)
# upperdir: 쓰기 가능 컨테이너 레이어
# merged: 합쳐진 최종 뷰 (컨테이너가 보는 파일시스템)
User Namespace 심화
User Namespace는 UID/GID를 격리하여 rootless 컨테이너를 가능하게 합니다. 컨테이너 내부의 root(UID 0)가 호스트에서는 일반 사용자(예: UID 100000)로 매핑됩니다.
# User Namespace 생성 (일반 사용자도 가능!)
unshare --user --map-root-user /bin/bash
# Namespace 내부:
id
# uid=0(root) gid=0(root) ← root처럼 보임
# 호스트에서 확인:
cat /proc/$PID/uid_map
# 0 1000 1
# → Namespace UID 0 = 호스트 UID 1000
# UID 매핑 구조
cat /proc/$PID/uid_map
# NS_UID HOST_UID RANGE
# 0 100000 65536
# → Namespace UID 0-65535 = 호스트 UID 100000-165535
# Kubernetes User Namespace (1.30+ beta)
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
hostUsers: false # User Namespace 활성화
containers:
- name: app
image: myapp
# 컨테이너 내부 root → 호스트에서는 비특권 사용자
# 컨테이너 탈출(escape)해도 호스트 root 권한 없음
unshare로 미니 컨테이너 만들기
Namespace를 조합하면 Docker 없이도 컨테이너와 유사한 격리 환경을 만들 수 있습니다:
#!/bin/bash
# 미니 컨테이너 생성 스크립트
ROOTFS="/var/lib/mini-container/rootfs"
# 1. Alpine rootfs 준비
mkdir -p $ROOTFS
curl -o /tmp/alpine.tar.gz
https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-minirootfs-3.19.1-x86_64.tar.gz
tar xzf /tmp/alpine.tar.gz -C $ROOTFS
# 2. 모든 Namespace를 격리하여 실행
unshare
--pid # PID 격리
--mount # Mount 격리
--uts # 호스트명 격리
--ipc # IPC 격리
--net # 네트워크 격리
--fork # 새 PID Namespace에서 fork
--mount-proc # /proc 새로 마운트
-- /bin/sh -c "
# 호스트명 설정
hostname mini-container
# pivot_root로 루트 변경
mount --bind $ROOTFS $ROOTFS
cd $ROOTFS
mkdir -p .old_root
pivot_root . .old_root
umount -l /.old_root
rmdir /.old_root
# /proc 마운트
mount -t proc proc /proc
exec /bin/sh
"
# → Docker와 거의 동일한 격리 환경!
# 부족한 것: cgroup 리소스 제한, seccomp, capabilities 제어
컨테이너 런타임과 Namespace
| 격리 수준 | 사용 기술 | 격리 강도 |
|---|---|---|
| Docker/containerd | Namespace + cgroup + seccomp + AppArmor | 프로세스 수준 (커널 공유) |
| gVisor (runsc) | Namespace + 유저 공간 커널 | 시스템콜 인터셉트 |
| Kata Containers | 경량 VM + Namespace | 하드웨어 수준 (커널 분리) |
| Firecracker | microVM | 하드웨어 수준 |
# Docker 컨테이너의 실제 Namespace 설정 확인
docker inspect mycontainer --format '{{json .HostConfig.NetworkMode}}'
# "bridge" → 별도 Network Namespace
docker inspect mycontainer --format '{{json .HostConfig.PidMode}}'
# "" → 별도 PID Namespace
# "host" → 호스트 PID Namespace 공유
# Kubernetes Pod의 Namespace 공유 범위
# 같은 Pod 내 컨테이너들이 공유하는 것:
# ✅ Network Namespace (같은 IP, localhost 통신)
# ✅ UTS Namespace (같은 호스트명)
# ✅ IPC Namespace (공유 메모리 가능)
# ❌ PID Namespace (기본 분리, shareProcessNamespace로 공유 가능)
# ❌ Mount Namespace (각자 파일시스템, Volume으로 공유)
# ❌ User Namespace (기본 분리)
보안 관점: Namespace 탈출
# ⚠️ 위험한 설정들 (Namespace 격리를 무력화)
# 1. --pid=host: 호스트의 모든 프로세스가 보임
docker run --pid=host alpine ps aux
# → 호스트 프로세스 전체 노출
# 2. --net=host: 호스트 네트워크 스택 공유
docker run --net=host alpine ip addr
# → 호스트 IP로 바인딩 가능
# 3. --privileged: 거의 모든 격리 해제
docker run --privileged alpine mount /dev/sda1 /mnt
# → 호스트 디스크 마운트 가능!
# 4. hostPID + hostNetwork in Kubernetes
apiVersion: v1
kind: Pod
spec:
hostPID: true # ❌ 프로덕션에서 피할 것
hostNetwork: true # ❌ 프로덕션에서 피할 것
# ✅ 권장: Pod Security Standards 적용
# Restricted 프로필은 위 설정들을 모두 차단
K8s Pod Security Standards 가이드에서 프로덕션 보안 정책을 확인하세요.
정리
Linux Namespace는 컨테이너 격리의 근본 기술입니다. PID Namespace로 프로세스 트리를, Network Namespace로 네트워크 스택을, Mount Namespace로 파일시스템을, User Namespace로 권한을 격리합니다. Namespace(가시성 격리) + cgroup(자원량 제한) + seccomp/AppArmor(시스템콜 제한)가 컨테이너 보안의 3대 축입니다. nsenter, lsns, unshare 명령어로 직접 Namespace를 조작해보면 컨테이너의 내부 동작을 체감할 수 있습니다.