네임스페이스 (Namespaces)
Linux 네임스페이스를 프로세스(Process) 가시성 격리(Isolation)의 핵심 메커니즘 관점에서 심층 분석합니다. PID/mount/network/user/IPC/UTS/cgroup/time 네임스페이스의 역할 분리, clone/unshare/setns 시스템 콜(System Call) 경로, user namespace와 capability 매핑(Mapping), 컨테이너(Container) 런타임의 네임스페이스 조합 전략, 호스트와의 경계 누수 위험과 방어 패턴, 디버깅(Debugging) 시 네임스페이스 진입 절차(nsenter/procfs), 운영 중 장애 원인 추적까지 실제 컨테이너 인프라에 필요한 핵심 내용을 다룹니다.
핵심 요약
- nsproxy — task가 참조하는 네임스페이스 집합
- clone/unshare/setns — 생성/분리/진입 API
- User NS — 권한 매핑과 공격면에 가장 큰 영향
- Mount/Net/PID NS — 컨테이너 격리의 실질 핵심
- pidfd — PID 재사용 문제를 줄이는 현대적 제어 방식
단계별 이해
- 개념 분해
어떤 자원이 어떤 네임스페이스에 의해 격리되는지 표로 정리합니다. - API 실습
unshare --mount --pid --fork같은 최소 예제로 동작을 확인합니다. - 런타임 연결
containerd/runc가 생성 단계에서 어떤 API를 호출하는지 추적합니다. - 보안 점검
User NS 허용 범위와 관련 sysctl 정책을 운영 환경에 맞춥니다.
네임스페이스 개요
네임스페이스(Namespace)는 커널 리소스를 격리하여 프로세스 그룹이 독립적인 시스템 뷰를 갖게 하는 메커니즘입니다. 컨테이너(Docker, LXC)의 핵심 기반 기술입니다. Linux 커널은 현재 8가지 네임스페이스를 제공하며, 각각이 서로 다른 종류의 시스템 자원을 격리합니다.
네임스페이스의 역사는 2002년 Linux 2.4.19에서 Mount namespace(CLONE_NEWNS)가 처음 도입된 것에서 시작합니다. 당시에는 "namespace"라는 개념 자체가 하나뿐이었기 때문에 플래그 이름이 단순히 CLONE_NEWNS("new namespace")가 되었고, 이 이름은 지금까지 그대로 유지되고 있습니다. 이후 UTS/IPC(2.6.19), PID(2.6.24), Network(2.6.29), User(3.8), Cgroup(4.6), Time(5.6) 네임스페이스가 순차적으로 추가되어 현재의 8종 체계가 완성되었습니다.
네임스페이스 vs 가상 머신
네임스페이스 기반 격리와 가상 머신(Virtual Machine) 기반 격리는 근본적으로 다른 계층에서 동작합니다. 이 차이를 이해하는 것이 컨테이너 보안 설계의 출발점입니다.
| 비교 항목 | 네임스페이스 (컨테이너) | 가상 머신 (KVM/QEMU) |
|---|---|---|
| 격리 계층 | 커널 자원의 뷰(View) 분리 | 하드웨어 수준의 완전 분리 |
| 커널 공유 | 호스트 커널을 공유 | 게스트 자체 커널 실행 |
| 시작 시간 | 밀리초 단위 | 초~분 단위 |
| 메모리 오버헤드 | 수 MB (프로세스 수준) | 수백 MB (OS 이미지 포함) |
| 밀도 | 호스트당 수백~수천 개 | 호스트당 수십 개 |
| 보안 경계 강도 | 약함 (커널 취약점에 노출) | 강함 (하이퍼바이저 경계) |
| syscall 경로 | 호스트 커널 직접 호출 | 게스트 커널 → 하이퍼바이저 |
| 파일시스템 | OverlayFS / bind mount | 가상 디스크 이미지 |
| 네트워크 | veth + bridge / macvlan | virtio-net / SR-IOV |
네임스페이스 도입 타임라인
네임스페이스 격리 원리
네임스페이스의 핵심 원리는 "같은 커널 위에서 서로 다른 시스템처럼 보이게 만든다"는 것입니다. 운영체제 수준의 가상화(Virtualization)가 아니라, 커널 자원에 대한 뷰(view)를 분리하는 것입니다. 각 네임스페이스는 커널 자료구조의 특정 부분에 대한 가시성 범위를 정의하며, 프로세스는 자기가 속한 네임스페이스 범위 내의 자원만 관찰하고 조작할 수 있습니다.
이 격리는 자원 자체를 복제하는 것이 아니라 자원에 대한 참조 경로를 분리하는 방식으로 구현됩니다. 예를 들어 PID 네임스페이스는 물리적인 프로세스를 복제하지 않고, 프로세스 ID의 번역 테이블을 계층적으로 관리합니다. 네트워크 네임스페이스는 물리적 네트워크 하드웨어를 복제하지 않고, 네트워크 스택의 소프트웨어 계층(라우팅 테이블(Routing Table), iptables 규칙, 소켓 바인딩 등)을 독립적으로 유지합니다.
커널 구현: nsproxy 구조체(Struct)
모든 프로세스(task_struct)는 nsproxy 포인터를 통해 자신이 속한 네임스페이스 집합을 참조합니다. 같은 네임스페이스를 공유하는 프로세스들은 동일한 nsproxy를 가리킵니다:
/* include/linux/nsproxy.h */
struct nsproxy {
refcount_t count; /* 참조 카운트 */
struct uts_namespace *uts_ns; /* 호스트명 */
struct ipc_namespace *ipc_ns; /* IPC 객체 */
struct mnt_namespace *mnt_ns; /* 마운트 포인트 */
struct pid_namespace *pid_ns_for_children; /* 자식의 PID NS */
struct net *net_ns; /* 네트워크 스택 */
struct time_namespace *time_ns; /* 시간 오프셋 */
struct time_namespace *time_ns_for_children;
struct cgroup_namespace *cgroup_ns; /* cgroup 뷰 */
};
/* task_struct에서 네임스페이스 접근 */
struct task_struct {
/* ... */
struct nsproxy *nsproxy; /* UTS/IPC/mount/net/time/cgroup 묶음 */
const struct cred *cred; /* cred->user_ns 가 권한 기준 */
struct fs_struct *fs; /* root/pwd 는 mount NS와 함께 해석 */
/* 현재 PID 뷰는 task_active_pid_ns(task) 경로로 별도 조회 */
};
중요한 점은 nsproxy가 "현재 task의 모든 격리 상태를 하나의 구조체에 완전히 담아 둔 것"은 아니라는 것입니다. 권한 판정은 cred->user_ns, 현재 PID 뷰는 task_active_pid_ns(task), 현재 경로 해석은 fs_struct와 mount namespace를 함께 봐야 정확하게 복원됩니다. 즉, 컨테이너 문맥을 조사할 때 task->nsproxy만 덤프(Dump)해서는 충분하지 않습니다.
네임스페이스 격리의 동작 방식은 다음과 같습니다:
- 자원 조회 시 네임스페이스 참조: 커널의 모든 자원 조회 경로(PID 조회, 네트워크 소켓(Socket) 생성, 마운트(Mount) 탐색 등)에서 현재 프로세스의
nsproxy를 참조하여 해당 네임스페이스 범위 내에서만 자원을 검색합니다. - Copy-on-Write 생성:
clone()이나unshare()시 지정된 네임스페이스만 새로 복사하고, 나머지는 부모와 공유합니다. 이는 생성 비용을 최소화합니다. - 참조 카운팅으로 수명 관리: 네임스페이스는 참조 카운트(Reference Count)가 0이 되면(마지막 프로세스 종료 + bind mount 없음) 자동으로 해제됩니다.
nsproxy
nsproxy는 namespace object 자체가 아니라 현재 task가 어떤 namespace 집합을 보고 있는지 가리키는 묶음 포인터입니다. 실제 격리 상태는 각 concrete namespace 객체(struct net, struct ipc_namespace, struct time_namespace 등)에 들어 있고, nsproxy는 그 객체들을 한 번에 task에 묶어 둡니다. 그래서 task가 네임스페이스 하나만 바꾼다고 해도 필드 하나만 덮어쓰는 방식보다 새 nsproxy를 만들고 필요한 포인터만 교체한 뒤 task의 nsproxy 포인터 자체를 바꾸는 설계가 선택됩니다.
이 간접 계층은 세 가지 면에서 중요합니다. 첫째, task_struct hot path에 namespace별 포인터를 낱개로 직접 박아 넣지 않고도 현재 격리 상태를 표현할 수 있습니다. 둘째, 대부분의 fork()/clone()은 새 네임스페이스를 만들지 않으므로 get_nsproxy(old) 한 번으로 빠르게 공유할 수 있습니다. 셋째, unshare()와 setns()가 "현재 task가 보는 namespace 묶음 전체"를 일관되게 교체할 수 있어, mount/net/ipc/user/time 같은 서로 다른 수명 규칙을 가진 객체들을 하나의 일괄 교체 단위처럼 취급할 수 있습니다.
현재 task가 namespace를 보는 실제 경로
실무에서 가장 많이 생기는 오해는 "task->nsproxy만 보면 현재 task의 namespace 문맥을 전부 알 수 있다"는 가정입니다. 실제로는 그렇지 않습니다. 대부분의 namespace는 nsproxy에서 시작하지만, PID는 활성 뷰와 자식 뷰가 분리되고, User NS는 cred, 경로 해석은 fs_struct가 추가로 개입합니다.
nsproxy가 출발점이지만, 현재 PID 뷰는 task_active_pid_ns(task), 권한은 cred->user_ns, 경로 해석은 fs_struct가 함께 관여합니다.| 조회 대상 | 실제 출발점 | 현재 task에서 의미 | 핵심 주의점 |
|---|---|---|---|
| UTS / IPC / Mount / Net / Cgroup | task->nsproxy | 현재 task가 바로 보는 자원 범위 | 대부분의 namespace는 이 경로에서 결정됩니다. |
| 현재 Time view | task->nsproxy->time_ns | 현재 task에 적용되는 monotonic / boottime 오프셋(Offset) | 읽기 경로는 현재 task의 time_ns를 즉시 참조합니다. |
| 현재 PID view | task_active_pid_ns(task) | 현재 task가 속한 활성 PID namespace | pid_ns_for_children와 동일하다고 가정하면 잘못된 진단이 나옵니다. |
| 자식 PID view | task->nsproxy->pid_ns_for_children | 다음 fork / clone 자식이 들어갈 PID namespace | unshare(CLONE_NEWPID) 직후 현재 task의 PID는 바뀌지 않습니다. |
| 자식 Time view | task->nsproxy->time_ns_for_children | 다음 자식이 상속할 time namespace | Time NS는 exec_task_namespaces()까지 엮이는 특수 경로가 있습니다. |
| User NS / capability | task->cred->user_ns | UID/GID 매핑과 권한 판정 기준 | User NS는 nsproxy가 아니라 cred에 붙습니다. |
| root / pwd | task->fs + task->nsproxy->mnt_ns | 현재 작업 디렉터리와 루트 디렉터리 | mount namespace만 바꿔도 fs_struct 정합성까지 맞춰야 합니다. |
또 하나 중요한 사실은 각 concrete namespace 객체가 다시 자기 소유 user_namespace를 들고 있다는 점입니다. 예를 들어 struct net, struct pid_namespace, struct ipc_namespace, struct time_namespace에는 보통 struct user_namespace *user_ns와 struct ns_common ns가 들어 있습니다. 즉 nsproxy는 "어떤 namespace 객체를 볼 것인가"를 결정하고, cred->user_ns는 "그 객체를 조작할 권한이 있는가"를 판정합니다.
예외: PID와 Time은 현재 뷰와 자식 뷰가 분리됩니다
nsproxy를 이해할 때 가장 자주 놓치는 부분이 바로 pid_ns_for_children와 time_ns_for_children입니다. 이름 그대로 이 둘은 현재 task 자체가 즉시 쓰는 namespace가 아니라, 앞으로 만들어질 자식에게 적용할 namespace입니다. 이 설계는 "이미 존재하는 task의 PID를 중간에 갈아끼울 수 없는" 현실과, Time NS가 vvar/vDSO와 맞물려 단계적으로 전환되어야 하는 제약에서 나옵니다.
- PID namespace: 현재 task의 PID는 이미 여러 테이블과 pid hash,
struct pid,thread_group연결에 박혀 있으므로,unshare(CLONE_NEWPID)가 현재 task를 새 PID NS로 바로 옮기지 않습니다. 대신pid_ns_for_children만 바꾸고, 다음fork()/clone()자식이 그 namespace의 PID 1 또는 그 이후 번호를 받습니다. - Time namespace: 현재 읽기 경로는
current->nsproxy->time_ns를 보지만, 자식용 후보는time_ns_for_children로 따로 관리됩니다. 최근 커널 트리에는exec_task_namespaces()가 있어time_ns_for_children != time_ns인 경우 exec 경계에서 현재 task의 time view를 정리해 전환합니다. - procfs 노출: 그래서
/proc/<pid>/ns/pid와/proc/<pid>/ns/pid_for_children,/proc/<pid>/ns/time과/proc/<pid>/ns/time_for_children가 따로 존재합니다. 현재 뷰와 다음 자식 뷰를 둘 다 관찰할 수 있어야 하기 때문입니다.
# 현재 task의 "즉시 적용된" namespace와 "다음 자식용" namespace를 같이 본다.
readlink /proc/self/ns/pid
readlink /proc/self/ns/pid_for_children
readlink /proc/self/ns/time
readlink /proc/self/ns/time_for_children
# PID namespace는 보통 자식을 만들어야 차이가 드러난다.
unshare --pid --fork --mount-proc /bin/sh
컨테이너 런타임이 초기 진입기(process bootstrap)를 별도로 두는 이유도 여기에 있습니다. PID namespace를 바꾸고 싶다면 기존 관리 프로세스 자신이 즉시 "새 PID 1"이 될 수는 없으므로, 보통 얇은 부모 프로세스가 새 namespace를 준비하고 진짜 workload 자식을 그 안에서 시작합니다.
공유, 복사, 해제 수명
nsproxy 자체의 count는 "이 nsproxy를 참조하는 task 수"를 뜻합니다. 반면 각 concrete namespace 객체의 refcount는 "이 namespace 객체를 가리키는 nsproxy 수"를 뜻합니다. 헤더 주석에서도 이 점을 분명히 말합니다. 따라서 스레드(Thread) 10개가 하나의 nsproxy를 공유해도, net_ns나 mnt_ns의 refcount는 task 수가 아니라 그 namespace를 가리키는 nsproxy 수를 기준으로 변합니다.
/* include/linux/nsproxy.h 의 핵심 규칙 요약 */
/* 1. current만 자신의 tsk->nsproxy 포인터를 바꿀 수 있다. */
/* 2. current의 namespace를 읽을 때는 그냥 역참조하면 된다. */
/* 3. 다른 task의 namespace를 읽을 때는 task_lock(task)가 필요하다. */
/* 4. task->nsproxy == NULL 이면 거의 종료 직전인 task일 수 있다. */
static inline void put_nsproxy(struct nsproxy *ns)
{
if (refcount_dec_and_test(&ns->count))
deactivate_nsproxy(ns);
}
최신 커널 트리의 kernel/nsproxy.c를 보면 deactivate_nsproxy()는 결국 put_mnt_ns(), put_uts_ns(), put_ipc_ns(), put_pid_ns(), put_time_ns(), put_cgroup_ns(), put_net()를 차례로 호출해 하위 namespace 객체들의 refcount를 내립니다. 즉 nsproxy가 마지막으로 해제되는 순간이, concrete namespace 객체들의 수명 감소가 시작되는 지점입니다.
- 공유 시점:
clone()/fork()에CLONE_NEW*플래그가 없으면 보통 기존nsproxy를 그대로 공유하고count만 증가합니다. - 복사 시점: namespace 하나라도 새로 만들거나 기존 다른 namespace에 진입하면 nsproxy 전체가 새로 준비됩니다. "필드 하나만 교체"가 아니라 "새 묶음을 만든 후 task에 스위치"하는 모델입니다.
- 종료 시점: task가 빠져나가면
switch_task_namespaces(tsk, NULL)같은 경로로put_nsproxy()가 호출됩니다. 최근 트리에는exit_nsproxy_namespaces()와exit_cred_namespaces()가 분리되어 있어, nsproxy 계열과 cred 계열 수명 정리가 나뉘어 있습니다. - 현재 task 읽기 vs 타 task 읽기: 자기 자신은 락 없이 읽을 수 있지만, 다른 task를 조사할 때는
task_lock()을 잡고task->nsproxy가 NULL이 아닌지 확인해야 합니다. 그렇지 않으면 종료 경쟁과 엇갈릴 수 있습니다.
clone, unshare, setns에서 nsproxy가 바뀌는 경로
최신 커널 트리 기준으로 nsproxy는 세 가지 큰 경로에서 움직입니다. clone()/clone3()는 자식 task를 만들면서 결정하고, unshare()는 현재 task의 문맥을 갈라내며, setns()는 기존 namespace로 현재 task를 이동시킵니다. 세 경로 모두 최종적으로는 "새 namespace 묶음을 준비한 뒤 switch_task_namespaces()로 교체"하는 형태를 취합니다.
/* clone 경로: copy_process() 내부 */
copy_process(...)
-> copy_namespaces(flags, child)
-> /* NEW* 플래그 없음 */ get_nsproxy(old)
-> /* NEW* 플래그 있음 */ create_new_namespaces(...)
-> child->nsproxy = new
/* unshare 경로: 현재 task 분리 */
ksys_unshare(...)
-> unshare_userns(...)
-> unshare_nsproxy_namespaces(...)
-> switch_task_namespaces(current, new_nsproxy)
/* setns 경로: fd 또는 pidfd 기반 진입 */
setns(fd, flags)
-> prepare_nsset(...)
-> validate_ns() / validate_nsset()
-> commit_nsset(...)
-> switch_task_namespaces(current, nsset.nsproxy)
clone()은 보통 공유 경로가 빠르고, unshare()와 setns()는 새 nsproxy 묶음을 준비한 뒤 최종 commit 단계에서 현재 task에 전환합니다.clone 경로: 최신 kernel/nsproxy.c의 copy_namespaces()는 CLONE_NEWNS, CLONE_NEWUTS, CLONE_NEWIPC, CLONE_NEWPID, CLONE_NEWNET, CLONE_NEWCGROUP, CLONE_NEWTIME가 하나도 없으면 빠르게 기존 nsproxy를 공유합니다. 새 namespace가 필요할 때만 create_new_namespaces()를 호출해 자식에게 새 묶음을 달아 줍니다.
unshare 경로: unshare()는 이미 실행 중인 current task를 바꾸므로 비활성 child에 대해 동작하는 copy_* 경로를 그대로 쓸 수 없습니다. 그래서 ksys_unshare()는 먼저 CLONE_NEWUSER면 CLONE_THREAD | CLONE_FS를 자동 포함시키고, CLONE_NEWNS면 CLONE_FS를 같이 요구하는 등 제약을 정리한 뒤, 새 cred, 새 fs_struct, 새 nsproxy를 준비해 마지막에 현재 task로 스위치합니다.
setns 경로: 최근 커널의 setns()는 오래된 "/proc/<pid>/ns/net 파일 디스크립터(File Descriptor) 하나" 방식뿐 아니라 pidfd + namespace bitmask 경로도 갖습니다. 내부적으로는 prepare_nsset()가 임시 nsset를 만들고, validate_ns() 또는 validate_nsset()가 대상 task의 nsproxy와 cred를 스냅샷으로 잡은 뒤, 모든 검증이 끝난 후 commit_nsset()가 commit_creds(), timens_commit(), switch_task_namespaces()를 순서대로 수행합니다. 즉 검증 실패로 중간 상태가 남지 않도록 일괄 적용하는 구조입니다.
/* include/linux/nsproxy.h - setns / unshare 지원용 보조 컨텍스트 */
struct nsset {
unsigned flags;
struct nsproxy *nsproxy;
struct fs_struct *fs;
const struct cred *cred;
};
int copy_namespaces(u64 flags, struct task_struct *tsk);
int unshare_nsproxy_namespaces(unsigned long flags,
struct nsproxy **new_nsp, struct cred *cred, struct fs_struct *fs);
void switch_task_namespaces(struct task_struct *tsk, struct nsproxy *new);
/proc/pid/ns, nsfs, bind mount로 namespace를 붙잡는 법
네임스페이스를 사용자 공간(User Space)에서 다루게 해 주는 접점은 /proc/<pid>/ns/*입니다. 최신 fs/proc/namespaces.c를 보면 procfs는 net, uts, ipc, pid, pid_for_children, user, mnt, cgroup, time, time_for_children 같은 항목을 노출합니다. 이 파일들은 보통 symlink처럼 보이지만, 실제로는 nsfs inode와 struct ns_common를 경유해 namespace 객체를 가리킵니다.
nsproxy는 task와 namespace 객체를 연결하고, nsfs는 그 namespace 객체를 파일 디스크립터로 바꿔 사용자 공간에서 보존하고 재진입할 수 있게 합니다.# 네임스페이스 식별자 확인
readlink /proc/self/ns/net
readlink /proc/self/ns/mnt
readlink /proc/self/ns/pid_for_children
# namespace를 파일 경로로 고정하여 수명 유지
mkdir -p /run/ns
mount --bind /proc/$TARGET_PID/ns/net /run/ns/demo-net
# 이후 task가 모두 종료해도 bind mount가 남아 있으면 namespace는 유지될 수 있다.
nsenter --net=/run/ns/demo-net ip link show
umount /run/ns/demo-net
운영 환경에서는 이 메커니즘이 자주 쓰입니다. CNI 플러그인이나 디버깅 도구가 컨테이너 네트워크 namespace를 잠시 보존하려고 /proc/$pid/ns/net를 bind mount해서 /var/run/netns/... 아래에 두는 방식이 대표적입니다. 이렇게 하면 원래 task가 죽어도 namespace 핸들이 남아 있어 추후 setns()나 nsenter로 재진입할 수 있습니다.
실전 디버깅 체크리스트
- "같은 컨테이너인지"를 inode로 확인:
readlink /proc/$pid/ns/*의name:[inum]을 비교하면 task들이 같은 namespace를 공유하는지 빠르게 볼 수 있습니다. - PID는 반드시 현재 뷰와 자식 뷰를 같이 본다:
pid와pid_for_children를 함께 봐야unshare(CLONE_NEWPID)직후의 상태를 오해하지 않습니다. - Time NS도 pair로 본다:
time과time_for_children의 inode가 다를 수 있으며, 이는 아직 fork / exec 경계가 지나지 않았음을 뜻할 수 있습니다. - setns 실패는 capability만의 문제가 아니다: 멀티스레드,
CLONE_FS공유, mount 전환 시 root / pwd 정합성,ptrace_may_access()조건 때문에도 자주 실패합니다. - mount namespace는 fs_struct까지 본다: namespace inode만 같아도
chroot,pivot_root,pwd차이 때문에 실제 파일 경로 해석은 달라질 수 있습니다. - User NS owner를 같이 본다: concrete namespace 객체는 자기 소유
user_ns를 갖고, 현재 task의cred->user_ns에 대해ns_capable()가 평가됩니다. UID 0이라고 해서 항상 허용되는 것이 아닙니다. - 수명 누수는 fd와 bind mount에서 생긴다: "프로세스는 다 죽었는데 namespace가 남아 있다"면 열린 fd, bind mount, 런타임의 namespace 핀 파일을 먼저 의심해야 합니다.
PID 번역 원리
PID 네임스페이스는 계층적 번역(hierarchical translation) 모델을 사용합니다. 하나의 프로세스가 여러 PID 네임스페이스에서 서로 다른 PID를 가질 수 있습니다:
/* 커널 내부: 프로세스는 레벨별로 PID를 가짐 */
struct pid {
refcount_t count;
unsigned int level; /* PID NS 깊이 */
struct upid numbers[]; /* 각 레벨별 PID 번호 */
};
struct upid {
int nr; /* 해당 NS에서의 PID 값 */
struct pid_namespace *ns; /* 소속 NS */
};
/* 예: 컨테이너 프로세스의 PID 매핑
* Host NS (level 0): PID 1234
* Container NS (level 1): PID 1
* → numbers[0] = {nr=1234, ns=host_ns}
* → numbers[1] = {nr=1, ns=container_ns}
*
* 자식 NS에서는 부모 NS의 PID를 볼 수 없지만,
* 부모 NS에서는 자식의 모든 프로세스를 볼 수 있음
*/
User 네임스페이스의 특수성: User NS는 nsproxy가 아닌 cred(자격 증명) 구조체에서 관리됩니다. 이는 UID/GID 매핑이 프로세스의 보안 컨텍스트에 직접 영향을 미치기 때문입니다. User NS는 다른 모든 네임스페이스의 "소유자(owner)"를 결정하며, 비특권 사용자도 User NS 내에서 다른 네임스페이스를 생성할 수 있게 합니다.
네임스페이스 유형
| 네임스페이스 | 격리 대상 | 시스템 콜 플래그 |
|---|---|---|
PID | 프로세스 ID | CLONE_NEWPID |
Mount | 마운트 포인트 | CLONE_NEWNS |
Network | 네트워크 스택(Network Stack) | CLONE_NEWNET |
User | UID/GID 매핑 | CLONE_NEWUSER |
UTS | 호스트명, 도메인명 | CLONE_NEWUTS |
IPC | System V IPC, POSIX MQ | CLONE_NEWIPC |
Cgroup | Cgroup 루트 디렉터리 | CLONE_NEWCGROUP |
Time | CLOCK_MONOTONIC 등 | CLONE_NEWTIME |
Time Namespace 상세
Time Namespace는 다른 네임스페이스와 달리 "자원 자체"가 아니라
CLOCK_MONOTONIC/CLOCK_BOOTTIME의 읽기 오프셋(Offset)을 격리합니다.
컨테이너 체크포인트(Checkpoint)/복원(CRIU) 시 시간축 일관성을 보장하는 것이 주목적입니다.
커널 5.6에서 도입된 가장 새로운 네임스페이스입니다.
Time NS 동작 메커니즘
/* kernel/time/namespace.c — Time NS 핵심 구조체 */
struct time_namespace {
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct ns_common ns;
struct timens_offsets offsets; /* 시간 오프셋 */
struct page *vvar_page; /* vDSO 매핑용 */
bool frozen_offsets; /* true면 오프셋 변경 불가 */
};
struct timens_offsets {
struct timespec64 monotonic; /* CLOCK_MONOTONIC 오프셋 */
struct timespec64 boottime; /* CLOCK_BOOTTIME 오프셋 */
};
/*
* Time NS 핵심 규칙:
* 1. CLOCK_REALTIME은 격리되지 않음 (NTP 동기화 필요)
* 2. 오프셋은 첫 프로세스 진입 전에만 설정 가능
* 3. frozen_offsets=true 이후 변경 불가
* 4. vDSO 페이지를 per-NS로 매핑하여 성능 유지
*/
# Time Namespace 사용 예시
# 1. Time NS 생성 (아직 진입 전)
unshare --time /bin/bash &
TIMENS_PID=$!
# 2. 오프셋 설정 (첫 프로세스 진입 전에만 가능)
# monotonic 시계를 100초 앞으로
echo "monotonic 100 0" > /proc/$TIMENS_PID/timens_offsets
# boottime 시계를 200초 앞으로
echo "boottime 200 0" > /proc/$TIMENS_PID/timens_offsets
# 3. 오프셋 확인
cat /proc/$TIMENS_PID/timens_offsets
# monotonic 100 0
# boottime 200 0
# 4. Time NS 내에서 시간 확인
nsenter --time --target $TIMENS_PID python3 -c "
import time
print('monotonic:', time.monotonic())
print('boottime:', time.clock_gettime(time.CLOCK_BOOTTIME))
print('realtime:', time.time()) # realtime은 같음
"
- 격리 대상:
CLONE_NEWTIME, monotonic/boottime 오프셋 - 비격리 대상:
CLOCK_REALTIME은 전역 시간 공유 - 운영 포인트: 오프셋은 첫 진입 이후 동결되므로 생성 순서 설계가 중요
- 기술 문서: 상세 API/제약/디버깅은 Time Namespaces 참고
네임스페이스 API 상세
네임스페이스를 다루는 핵심 시스템 콜은 세 가지입니다: clone()/clone3()(생성과 동시에 자식 프로세스 배치), unshare()(현재 프로세스를 새 네임스페이스로 분리), setns()(기존 네임스페이스에 진입). 각 API의 정확한 사용법과 주의사항을 정리합니다.
clone() / clone3() — 네임스페이스 생성과 프로세스 생성
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/mount.h>
#define STACK_SIZE (1024 * 1024)
static int child_fn(void *arg)
{
/* 새 PID/Mount/UTS 네임스페이스 안에서 실행됨 */
/* 호스트명 변경 (UTS NS 격리로 호스트에 영향 없음) */
sethostname("container", 9);
/* /proc 재마운트 (PID NS에 맞는 프로세스 목록 표시) */
mount("proc", "/proc", "proc", 0, NULL);
char hostname[64];
gethostname(hostname, sizeof(hostname));
printf("[child] hostname=%s, PID=%d\n", hostname, getpid());
/* 자식 프로세스의 작업 수행 */
execl("/bin/sh", "sh", NULL);
return 1;
}
int main(void)
{
char *stack = malloc(STACK_SIZE);
if (!stack) return 1;
/* clone()으로 새 PID/Mount/UTS 네임스페이스에서 자식 생성 */
pid_t child_pid = clone(
child_fn,
stack + STACK_SIZE, /* 스택 끝 (아래로 자람) */
CLONE_NEWPID | /* 새 PID 네임스페이스 */
CLONE_NEWNS | /* 새 Mount 네임스페이스 */
CLONE_NEWUTS | /* 새 UTS 네임스페이스 */
SIGCHLD, /* 자식 종료 시 SIGCHLD */
NULL /* child_fn 인자 */
);
if (child_pid == -1) {
perror("clone");
return 1;
}
printf("[parent] child PID in host NS: %d\n", child_pid);
waitpid(child_pid, NULL, 0);
free(stack);
return 0;
}
clone3() — 현대적 네임스페이스 생성
#define _GNU_SOURCE
#include <linux/sched.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
int pidfd = -1;
struct clone_args args = {
.flags = CLONE_NEWPID /* 새 PID NS */
| CLONE_NEWNET /* 새 Network NS */
| CLONE_NEWNS /* 새 Mount NS */
| CLONE_NEWUTS /* 새 UTS NS */
| CLONE_NEWIPC /* 새 IPC NS */
| CLONE_NEWCGROUP /* 새 Cgroup NS */
| CLONE_PIDFD, /* pidfd 자동 생성 */
.pidfd = (__u64)(&pidfd),
.exit_signal = SIGCHLD,
};
pid_t child = syscall(SYS_clone3, &args, sizeof(args));
if (child == 0) {
/* 자식: 새 네임스페이스 안 */
printf("[child] PID=%d (네임스페이스 내부)\n", getpid());
sleep(60);
return 0;
}
printf("[parent] child PID=%d, pidfd=%d\n", child, pidfd);
/* pidfd로 안전한 시그널 전송 (PID 재사용 문제 없음) */
syscall(SYS_pidfd_send_signal, pidfd, SIGTERM, NULL, 0);
/* pidfd로 네임스페이스 진입도 가능 (5.8+) */
/* setns(pidfd, CLONE_NEWNET | CLONE_NEWPID); */
waitpid(child, NULL, 0);
close(pidfd);
return 0;
}
/*
* clone3() vs clone() 차이:
* - clone3()는 구조체 기반 → 확장 용이 (새 필드 추가 가능)
* - CLONE_PIDFD로 pidfd 자동 생성
* - set_tid[]로 특정 PID 번호 지정 가능 (CRIU 복원용)
* - cgroup 지정 가능 (CLONE_INTO_CGROUP, 5.7+)
* - clone()의 스택 포인터 직접 관리 불필요
*/
unshare() — 현재 프로세스 네임스페이스 분리
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mount.h>
int main(void)
{
printf("[before] PID=%d\n", getpid());
/* 현재 프로세스를 새 Mount/UTS NS로 분리 */
if (unshare(CLONE_NEWNS | CLONE_NEWUTS) == -1) {
perror("unshare");
return 1;
}
/* Mount propagation을 private으로 설정 */
mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL);
/* 호스트명 변경 (호스트에 영향 없음) */
sethostname("isolated", 8);
/* 임시 파일시스템 마운트 (호스트에 영향 없음) */
mount("tmpfs", "/tmp", "tmpfs", 0, "size=64M");
printf("[after] PID=%d (같은 PID, 다른 namespace)\n", getpid());
/* 주의: unshare(CLONE_NEWPID)는 현재 프로세스의
* PID를 변경하지 않음! 다음 fork()의 자식만 새 PID NS에 배치됨 */
execl("/bin/sh", "sh", NULL);
return 1;
}
setns() — 기존 네임스페이스 진입
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <unistd.h>
/* 방법 1: /proc/PID/ns/* 파일 디스크립터 사용 */
int enter_ns_via_proc(pid_t target_pid)
{
char path[256];
int fd;
/* Network NS 진입 */
snprintf(path, sizeof(path), "/proc/%d/ns/net", target_pid);
fd = open(path, O_RDONLY);
if (fd == -1) { perror("open net ns"); return -1; }
if (setns(fd, CLONE_NEWNET) == -1) { perror("setns net"); return -1; }
close(fd);
/* Mount NS 진입 */
snprintf(path, sizeof(path), "/proc/%d/ns/mnt", target_pid);
fd = open(path, O_RDONLY);
if (fd == -1) { perror("open mnt ns"); return -1; }
if (setns(fd, CLONE_NEWNS) == -1) { perror("setns mnt"); return -1; }
close(fd);
/* 주의: 여러 NS를 순차적으로 진입하면
* 중간 상태(일부만 전환된 불일치)가 발생할 수 있음 */
return 0;
}
/* 방법 2: pidfd로 여러 NS를 원자적 진입 (5.8+, 권장) */
int enter_ns_via_pidfd(pid_t target_pid)
{
int pidfd = syscall(SYS_pidfd_open, target_pid, 0);
if (pidfd == -1) { perror("pidfd_open"); return -1; }
/* 한 번의 setns()로 여러 NS를 원자적으로 진입 */
if (setns(pidfd, CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWPID) == -1) {
perror("setns pidfd");
close(pidfd);
return -1;
}
close(pidfd);
/* 이제 target_pid의 Net/Mount/PID NS 안에 있음 */
return 0;
}
/* 방법 3: bind mount된 NS 파일로 진입 */
int enter_ns_via_bind(const char *ns_path)
{
/* ip netns add myns → /var/run/netns/myns 파일 생성 */
int fd = open(ns_path, O_RDONLY);
if (fd == -1) { perror("open bind ns"); return -1; }
if (setns(fd, 0) == -1) { perror("setns"); return -1; }
close(fd);
return 0;
}
CLONE_NEW* 플래그 상세 비교
| 플래그 | 격리 대상 | clone() | unshare() | setns() | 특이사항 |
|---|---|---|---|---|---|
CLONE_NEWPID | PID | 자식이 새 NS의 PID 1 | 다음 fork() 자식에만 적용 | 다음 fork() 자식에만 적용 | 현재 PID는 변하지 않음 |
CLONE_NEWNS | 마운트 | 마운트 트리 복사 | 마운트 트리 복사 | 즉시 적용 | 생성 비용 높음 (마운트 수 비례) |
CLONE_NEWNET | 네트워크 | lo만 있는 빈 스택 | lo만 있는 빈 스택 | 즉시 적용 | 열린 소켓은 원래 NS에 유지 |
CLONE_NEWUSER | UID/GID | 즉시 새 User NS | 즉시 새 User NS | 즉시 적용 | 비특권 사용자도 생성 가능 |
CLONE_NEWUTS | 호스트명 | 복사 후 격리 | 복사 후 격리 | 즉시 적용 | 매우 가벼움 |
CLONE_NEWIPC | IPC | 빈 IPC 테이블 | 빈 IPC 테이블 | 즉시 적용 | 기존 IPC 객체는 복사 안 함 |
CLONE_NEWCGROUP | cgroup 뷰 | 현재 위치가 루트 | 현재 위치가 루트 | 즉시 적용 | 가시성만 격리 |
CLONE_NEWTIME | 시간 오프셋 | 자식에 적용 | 다음 exec()에 적용 | 다음 exec()에 적용 | vDSO 재매핑 필요 |
PID Namespace
PID 네임스페이스는 프로세스 ID를 격리합니다. 새 PID NS 내의 첫 프로세스는 PID 1이 되며, 부모 NS에서는 다른 PID로 보입니다. PID 네임스페이스는 계층적(hierarchical) 구조를 형성하며, 부모 네임스페이스에서는 자식 네임스페이스의 모든 프로세스를 볼 수 있지만, 자식에서는 부모의 프로세스를 볼 수 없습니다.
PID 1 (init 프로세스)의 특수성
각 PID 네임스페이스의 PID 1 프로세스는 그 네임스페이스의 init 프로세스 역할을 합니다. 이 프로세스는 두 가지 중요한 특성을 가집니다:
- 고아 프로세스(Orphan Process) 수양(Reaping): 네임스페이스 내의 프로세스가 부모보다 먼저 종료되거나, 부모가 먼저 종료되면 고아 프로세스가 됩니다. 이 고아 프로세스들은 해당 PID NS의 init 프로세스에 재배정(re-parenting)됩니다. init 프로세스가
wait()로 이들을 수거하지 않으면 좀비 프로세스(Zombie Process)가 누적됩니다. - 시그널 보호: PID NS의 init 프로세스는 해당 네임스페이스 내의 다른 프로세스가 보내는 시그널 중, init이 명시적으로 핸들러(Handler)를 등록하지 않은 시그널을 무시합니다. 단, 부모 네임스페이스에서 보내는
SIGKILL과SIGSTOP은 예외입니다.
/* kernel/pid_namespace.c — PID NS init 프로세스 종료 시 처리 */
/*
* PID NS의 init(PID 1)이 종료하면:
* 1. 해당 NS 내의 모든 프로세스에 SIGKILL 전송
* 2. NS를 더 이상 사용할 수 없도록 표시
* 3. 이미 unshare(CLONE_NEWPID)로 준비된 자식 NS도 무효화
*
* 따라서 컨테이너의 PID 1이 죽으면 컨테이너 전체가 종료됩니다.
* 이것이 컨테이너에서 tini, dumb-init 같은
* 경량 init 프로세스를 사용하는 이유입니다.
*/
/* 시그널 필터링: PID NS init은 핸들러 미등록 시그널을 무시 */
/* kernel/signal.c — sig_task_ignored() 내부 */
static bool sig_task_ignored(struct task_struct *t, int sig, bool force)
{
/* ... */
if (is_child_reaper(task_pid(t))) {
/* init 프로세스: 명시적 핸들러가 없으면 시그널 무시 */
if (handler == SIG_DFL && !(force && sig_kernel_only(sig)))
return 1;
}
/* ... */
}
PID 네임스페이스 중첩과 /proc 마운트
PID 네임스페이스는 최대 32단계까지 중첩할 수 있습니다(MAX_PID_NS_LEVEL). 중첩 시 프로세스는 자기가 속한 네임스페이스부터 루트 네임스페이스까지 각 레벨에서 서로 다른 PID를 가집니다. /proc 파일시스템은 특정 PID 네임스페이스에 바인딩(Binding)되므로, 새 PID NS를 만든 후에는 /proc를 다시 마운트해야 해당 네임스페이스의 프로세스 목록이 정확하게 표시됩니다.
# PID 네임스페이스 생성 + /proc 재마운트
unshare --pid --fork --mount-proc /bin/bash
ps aux
# PID 1 = bash, PID 2 = ps (이 NS 내의 프로세스만 보임)
# --mount-proc 없이 생성하면 /proc가 호스트의 것을 보여줌
unshare --pid --fork /bin/bash
ps aux
# 호스트의 전체 프로세스가 보임 (잘못된 뷰)
# 수동으로 /proc 재마운트:
mount -t proc proc /proc
ps aux
# 이제 올바른 PID NS 뷰
# 중첩된 PID 네임스페이스에서 PID 확인
echo "PID NS level 0 (Host): $$"
unshare --pid --fork --mount-proc /bin/bash -c '
echo "PID NS level 1: $$"
grep NSpid /proc/self/status
# NSpid: 호스트PID 레벨1PID
'
struct pid의 numbers[] 배열을 통해 각 PID NS 레벨에서 서로 다른 PID를 가집니다. 부모 NS에서는 모든 자식 NS의 프로세스를 볼 수 있습니다./* PID 번역 API — 커널 내부에서 사용 */
/* 특정 PID NS에서의 PID 번호 조회 */
pid_t nr = task_pid_nr_ns(task, target_ns);
/* 현재 PID NS 기준 PID (가장 자주 사용) */
pid_t nr = task_pid_vnr(task);
/* PID 번호로 task 찾기 (특정 NS 내에서) */
struct task_struct *task = find_task_by_pid_ns(nr, ns);
/* 현재 task의 활성 PID NS 확인 */
struct pid_namespace *ns = task_active_pid_ns(current);
/*
* 주의: task_active_pid_ns(task)와
* task->nsproxy->pid_ns_for_children는 다를 수 있음!
* unshare(CLONE_NEWPID) 직후에는
* 현재 task의 PID는 변하지 않고,
* pid_ns_for_children만 새 NS를 가리킴
*/
Network Namespace
네트워크 네임스페이스는 네트워크 인터페이스, 라우팅 테이블(Routing Table), iptables/nftables 규칙, 소켓, ARP 테이블, /proc/net 등 네트워크 스택(Network Stack) 전체를 완전히 격리합니다. 새 네트워크 네임스페이스는 루프백(Loopback) 인터페이스만 가지고 시작하며, 다른 인터페이스는 명시적으로 이동시키거나 생성해야 합니다.
struct net 내부 구조
/* include/net/net_namespace.h — 네트워크 네임스페이스의 핵심 구조체 */
struct net {
refcount_t passive; /* 수동 참조 카운트 */
refcount_t count; /* 활성 참조 카운트 */
struct list_head list; /* 전역 net namespace 리스트 */
struct list_head exit_list; /* cleanup 콜백 리스트 */
struct ns_common ns; /* nsfs 연결 */
struct user_namespace *user_ns; /* 소유 User NS */
struct net_device *loopback_dev; /* lo 인터페이스 */
struct netns_ipv4 ipv4; /* IPv4 상태 (라우팅, conntrack, ...) */
struct netns_ipv6 ipv6; /* IPv6 상태 */
struct netns_nf nf; /* netfilter 상태 */
struct netns_nf_frag nf_frag; /* 단편화 처리 */
/* 각 네트워크 서브시스템의 per-netns 데이터 */
struct proc_dir_entry *proc_net; /* /proc/net */
struct sock *genl_sock; /* Generic Netlink */
struct sock *rtnl; /* Routing Netlink */
/* ... */
};
/*
* 각 net_device(네트워크 인터페이스)는 정확히 하나의 net에 속합니다.
* dev_change_net_namespace()로 인터페이스를 다른 netns로 이동 가능.
* 하지만 물리 인터페이스를 이동하면 원래 netns에서 사라집니다.
*/
네트워크 네임스페이스 간 연결 방식
| 연결 방식 | 구현 | 주요 용도 | 성능 특성 |
|---|---|---|---|
veth 페어 | 가상 이더넷 쌍, 한쪽 출력이 다른 쪽 입력 | 컨테이너 기본 연결 | 커널 내 메모리 복사, 중간 수준 |
macvlan | 물리 인터페이스에 가상 MAC 할당 | VM과 유사한 네트워크 모델 | 브릿지(Bridge) 우회로 빠름 |
ipvlan | 물리 인터페이스의 IP를 공유 | MAC 주소 제한 환경 | L3 모드에서 매우 빠름 |
| 물리 인터페이스 이동 | ip link set dev ethX netns ... | SR-IOV VF, 전용 NIC | 하드웨어 직접 접근으로 최고 |
# 네트워크 네임스페이스 생성
ip netns add myns
# veth 페어 생성 및 연결
ip link add veth0 type veth peer name veth1
ip link set veth1 netns myns
# 네임스페이스 내에서 명령 실행
ip netns exec myns ip addr add 10.0.0.2/24 dev veth1
ip netns exec myns ip link set veth1 up
ip netns exec myns ip link set lo up
# 호스트 측 veth 설정
ip addr add 10.0.0.1/24 dev veth0
ip link set veth0 up
# 연결 확인
ping -c 1 10.0.0.2
# 네임스페이스에서 외부 통신을 위한 NAT 설정
ip netns exec myns ip route add default via 10.0.0.1
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
echo 1 > /proc/sys/net/ipv4/ip_forward
브릿지를 통한 다중 컨테이너 연결
실제 컨테이너 환경에서는 여러 컨테이너를 Linux 브릿지(Bridge)로 연결하는 것이 일반적입니다. Docker의 기본 네트워크 모델이 바로 이 방식입니다.
# Docker 스타일 네트워크: bridge + veth + NAT
# 1. 브릿지 생성
ip link add docker0 type bridge
ip addr add 172.17.0.1/16 dev docker0
ip link set docker0 up
# 2. 컨테이너 A 네트워크 설정
ip netns add container_a
ip link add veth-a-host type veth peer name veth-a-container
ip link set veth-a-container netns container_a
ip link set veth-a-host master docker0
ip link set veth-a-host up
ip netns exec container_a ip addr add 172.17.0.2/16 dev veth-a-container
ip netns exec container_a ip link set veth-a-container up
ip netns exec container_a ip link set lo up
ip netns exec container_a ip route add default via 172.17.0.1
# 3. 컨테이너 B 네트워크 설정
ip netns add container_b
ip link add veth-b-host type veth peer name veth-b-container
ip link set veth-b-container netns container_b
ip link set veth-b-host master docker0
ip link set veth-b-host up
ip netns exec container_b ip addr add 172.17.0.3/16 dev veth-b-container
ip netns exec container_b ip link set veth-b-container up
ip netns exec container_b ip link set lo up
ip netns exec container_b ip route add default via 172.17.0.1
# 4. NAT 설정 (외부 통신용)
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
echo 1 > /proc/sys/net/ipv4/ip_forward
# 5. 컨테이너 간 통신 확인
ip netns exec container_a ping -c 1 172.17.0.3
# 컨테이너 A → docker0 bridge → 컨테이너 B
# 6. 외부 통신 확인
ip netns exec container_a ping -c 1 8.8.8.8
# 컨테이너 A → docker0 → NAT → eth0 → 외부
lsns 명령으로 시스템의 모든 네임스페이스를 확인할 수 있습니다. /proc/[pid]/ns/ 디렉터리에서 프로세스가 속한 네임스페이스의 파일 디스크립터를 얻을 수 있습니다.
네트워크 네임스페이스 커널 구현 상세
네트워크 네임스페이스의 커널 구현은 struct net를 중심으로 네트워크 스택 전체를 per-namespace로 관리합니다. 네트워크 장치(net_device), 소켓, 라우팅 테이블, netfilter 규칙 등이 모두 특정 struct net에 바인딩됩니다.
/* 네트워크 네임스페이스에서 장치를 이동시키는 커널 내부 흐름 */
/* net/core/dev.c — dev_change_net_namespace() 핵심 단계 */
int dev_change_net_namespace(struct net_device *dev,
struct net *net,
const char *pat)
{
/* 1. 장치를 down 상태로 전환 */
dev_close(dev);
/* 2. 기존 netns에서 해시 테이블 제거 */
unlist_netdevice(dev);
/* 3. 장치의 netns 포인터를 새 netns로 변경 */
dev_net_set(dev, net);
/* 4. 새 netns의 해시 테이블에 추가 */
list_netdevice(dev);
/* 5. 이름 충돌 처리 (새 netns에 같은 이름이 있으면 변경) */
if (pat)
dev_change_name(dev, pat);
/* 6. 장치를 up 상태로 복원 (선택적) */
return 0;
}
/*
* 이동 불가능한 장치:
* - 물리 인터페이스: 이동 가능하지만 원래 netns에서 사라짐
* - loopback (lo): 이동 불가 (각 netns에 고유)
* - bridge/bond의 member: master에서 분리 후 이동
* - NETIF_F_NETNS_LOCAL 플래그 장치: 이동 불가
* (lo, vxlan, bridge 등)
*/
소켓과 네트워크 네임스페이스
소켓은 생성 시점의 네트워크 네임스페이스에 바인딩됩니다. setns()로 네트워크 네임스페이스를 변경해도 이미 열린 소켓은 원래 네임스페이스에 남아 있습니다. 새 네임스페이스에서 사용할 소켓은 전환 후에 새로 생성해야 합니다.
/* 소켓과 netns 관계 */
struct sock {
/* ... */
struct net *sk_net; /* 이 소켓이 속한 netns */
/* ... */
};
/* 소켓 생성 시 현재 task의 netns가 자동으로 설정됨 */
/* socket() → sock_create() → sk_alloc() → sock_net_set(sk, net) */
/*
* 실무 시나리오: 디버깅 도구가 컨테이너 네트워크를 조사할 때
* 1. setns()로 컨테이너 net NS에 진입
* 2. 새 소켓을 생성하여 네트워크 상태 조회
* 3. 기존 소켓은 원래 NS에 있으므로 사용 불가
*/
고급 네트워크 토폴로지
# === macvlan: VM과 유사한 네트워크 모델 ===
# 물리 인터페이스에 가상 MAC 주소를 할당
ip link add macvlan0 link eth0 type macvlan mode bridge
ip link set macvlan0 netns container_a
ip netns exec container_a ip addr add 192.168.1.100/24 dev macvlan0
ip netns exec container_a ip link set macvlan0 up
# 장점: 브릿지 없이 직접 물리 네트워크에 연결
# 단점: 호스트와 macvlan 컨테이너 간 통신 제한
# === ipvlan L3 모드: 최고 성능 ===
ip link add ipvlan0 link eth0 type ipvlan mode l3
ip link set ipvlan0 netns container_b
ip netns exec container_b ip addr add 10.0.1.1/32 dev ipvlan0
ip netns exec container_b ip route add default dev ipvlan0
# 장점: MAC 주소 절약, L3 라우팅으로 빠름
# 단점: ARP/NDP 불가, L2 프로토콜 사용 불가
# === SR-IOV VF 할당: 하드웨어 직접 접근 ===
# 1. SR-IOV VF 생성
echo 4 > /sys/class/net/enp5s0f0/device/sriov_numvfs
# 2. VF를 컨테이너 netns로 이동
ip link set enp5s0f0v0 netns container_c
ip netns exec container_c ip addr add 10.0.2.1/24 dev enp5s0f0v0
ip netns exec container_c ip link set enp5s0f0v0 up
# 장점: 하드웨어 오프로드, 최고 성능
# 단점: SR-IOV 지원 NIC 필요
Mount Namespace
Mount namespace는 프로세스별로 독립적인 마운트 포인트 테이블을 제공합니다. 컨테이너가 호스트와 다른 파일시스템(Filesystem) 뷰를 가질 수 있게 합니다. 내부적으로 각 Mount NS는 자체 struct mnt_namespace를 가지며, 이 안에 마운트 포인트의 트리 구조가 저장됩니다. Mount namespace는 8가지 네임스페이스 중 생성 비용이 가장 높은 네임스페이스입니다. 기존 마운트 트리 전체를 복사하기 때문입니다.
Mount Namespace 내부 구조
/* fs/mount.h — 마운트 네임스페이스 구조체 */
struct mnt_namespace {
refcount_t count; /* 참조 카운트 */
struct ns_common ns; /* nsfs 연결 */
struct mount *root; /* 이 NS의 루트 마운트 */
struct rb_root mounts; /* red-black tree of mounts */
struct user_namespace *user_ns; /* 소유 User NS */
struct ucounts *ucounts; /* 리소스 카운터 */
u64 seq; /* 시퀀스 번호 */
unsigned int nr_mounts; /* 현재 마운트 수 */
unsigned int pending_mounts; /* 생성 중인 마운트 */
};
/* fs/mount.h — 각 마운트 포인트 구조체 */
struct mount {
struct hlist_node mnt_hash; /* 해시 테이블 연결 */
struct mount *mnt_parent; /* 부모 마운트 */
struct dentry *mnt_mountpoint; /* 마운트 위치 */
struct vfsmount mnt; /* VFS 마운트 정보 */
struct mnt_namespace *mnt_ns; /* 소속 namespace */
/* mount propagation 관련 */
struct list_head mnt_share; /* shared peer group */
struct list_head mnt_slave_list; /* slave 마운트 리스트 */
struct list_head mnt_slave; /* slave 리스트 내 연결 */
struct mount *mnt_master; /* slave의 master 마운트 */
int mnt_group_id; /* peer group ID */
/* ... */
};
pivot_root: 컨테이너 루트 전환
pivot_root()는 컨테이너의 루트 파일시스템을 설정하는 핵심 시스템 콜입니다. chroot()와 달리 pivot_root()는 실제 마운트 포인트를 교체하므로 이전 루트에 대한 참조를 완전히 제거할 수 있어 보안상 더 강력합니다.
/* 컨테이너 런타임의 pivot_root 사용 패턴 (runc 기반) */
static int setup_rootfs(const char *rootfs)
{
/* 1. 새 루트를 bind mount (자기 자신에게) */
mount(rootfs, rootfs, NULL, MS_BIND | MS_REC, NULL);
/* 2. 이전 루트를 담을 디렉터리 생성 */
char oldroot[PATH_MAX];
snprintf(oldroot, sizeof(oldroot), "%s/.pivot_root", rootfs);
mkdir(oldroot, 0700);
/* 3. 루트 전환: 새 루트 ↔ 이전 루트 교체 */
pivot_root(rootfs, oldroot);
/* 4. 작업 디렉터리를 새 루트로 이동 */
chdir("/");
/* 5. 이전 루트 언마운트 및 제거 */
umount2("/.pivot_root", MNT_DETACH);
rmdir("/.pivot_root");
/* 이제 컨테이너는 호스트 파일시스템에 접근할 수 없음 */
return 0;
}
/*
* chroot vs pivot_root:
* - chroot: 경로 해석의 시작점만 변경, /proc/1/root로 탈출 가능
* - pivot_root: 실제 마운트 포인트 교체, 이전 루트 완전 분리 가능
* - 컨테이너 보안상 pivot_root + umount 조합이 권장됨
*/
Mount Propagation 원리
Mount namespace 간의 마운트 이벤트 전파는 peer group 메커니즘으로 제어됩니다. 새 Mount NS를 생성하면 기본적으로 부모와 peer group을 형성하여 마운트 이벤트가 양방향으로 전파됩니다. propagation 유형에 따라 이 동작을 제어합니다:
/* 새 mount namespace에서 프로세스 생성 */
int flags = CLONE_NEWNS;
unshare(flags);
/* pivot_root: 컨테이너의 루트 파일시스템 변경 */
pivot_root("./newroot", "./newroot/oldroot");
umount2("/oldroot", MNT_DETACH);
# mount namespace 생성 및 확인
unshare --mount --propagation private /bin/bash
mount --bind /tmp/myroot /mnt
# 호스트에서는 /mnt가 변경되지 않음
# mount propagation 유형
mount --make-shared /mnt # 양방향 전파
mount --make-slave /mnt # 단방향 (호스트→컨테이너)
mount --make-private /mnt # 전파 없음
mount --make-unbindable /mnt # bind mount도 불가
User Namespace
User namespace는 UID/GID 매핑을 격리합니다. 컨테이너 내에서 root(UID 0)로 보이지만 호스트에서는 일반 사용자입니다. User NS는 다른 모든 namespace와 근본적으로 다른 위치를 차지합니다. nsproxy가 아니라 cred(자격 증명)에 부착되며, 나머지 namespace들의 "소유자(owner)"를 결정하는 계층 구조의 루트 역할을 합니다.
User NS 계층 구조와 소유 관계
User namespace는 트리 계층을 형성합니다. 새 User NS를 만들면 현재 User NS가 부모가 됩니다. 모든 non-user namespace 객체(struct net, struct pid_namespace, struct mnt_namespace 등)는 생성 시점의 User NS를 소유자로 기록합니다. 이 소유 관계가 ns_capable() 권한 판정의 핵심입니다.
/* include/linux/user_namespace.h */
struct user_namespace {
struct uid_gid_map uid_map; /* 컨테이너 UID → 부모 UID 변환 */
struct uid_gid_map gid_map; /* 컨테이너 GID → 부모 GID 변환 */
struct uid_gid_map projid_map; /* 프로젝트 ID 매핑 */
struct user_namespace *parent; /* 부모 User NS (계층 트리) */
int level; /* 중첩 깊이 (init_user_ns=0) */
kuid_t owner; /* 생성자의 UID (부모 NS 기준) */
kgid_t group; /* 생성자의 GID (부모 NS 기준) */
struct ns_common ns; /* nsfs 연결 */
unsigned long flags; /* USERNS_SETGROUPS_ALLOWED 등 */
struct ucounts *ucounts; /* 리소스 카운터 */
int ucount_max[UCOUNT_COUNTS]; /* 리소스 제한 */
/* ... */
};
/* 각 non-user namespace가 소유자 User NS를 가리키는 패턴 */
struct net {
/* ... */
struct user_namespace *user_ns; /* 이 net NS를 소유하는 User NS */
struct ns_common ns;
};
struct pid_namespace {
/* ... */
struct user_namespace *user_ns; /* 이 PID NS를 소유하는 User NS */
struct ns_common ns;
};
ns_capable()는 현재 task의 cred->user_ns가 대상 namespace의 소유 User NS와 같거나 그 조상이어야 capability를 인정합니다.User NS 내부의 Capability 모델
User namespace의 가장 강력한 특성은 비특권 사용자가 새 User NS를 만들면 그 내부에서 모든 capability를 자동으로 획득한다는 것입니다. 그러나 이 capability는 해당 User NS가 소유하는 namespace에 대해서만 효력이 있습니다.
/* kernel/capability.c — ns_capable의 핵심 판정 로직 */
bool ns_capable(struct user_namespace *ns, int cap)
{
return ns_capable_common(ns, cap, CAP_OPT_NONE);
}
/*
* 내부적으로 current->cred->user_ns 에서 시작하여
* 대상 ns까지 parent 체인을 올라갑니다.
* current의 user_ns가 대상 ns와 같거나 조상이면 → cap 검사
* 조상이 아니면 → 무조건 실패
*
* 예: 컨테이너 user_ns_A 내부의 root가
* 호스트의 net_ns를 조작하려면 → 실패
* user_ns_A가 소유하는 net_ns를 조작하려면 → 성공
*/
| 시나리오 | current user_ns | 대상 namespace 소유자 | capability 인정 | 이유 |
|---|---|---|---|---|
| 호스트 root가 호스트 net_ns 조작 | init_user_ns | init_user_ns | 인정 | 같은 user_ns |
| 컨테이너 root가 자기 net_ns 조작 | user_ns_A | user_ns_A | 인정 | 같은 user_ns |
| 컨테이너 root가 호스트 net_ns 조작 | user_ns_A | init_user_ns | 거부 | user_ns_A는 init_user_ns의 자식 |
| 호스트 root가 컨테이너 net_ns 조작 | init_user_ns | user_ns_A | 인정 | init_user_ns는 user_ns_A의 조상 |
UID/GID 매핑 내부 구현
UID 매핑은 /proc/<pid>/uid_map과 /proc/<pid>/gid_map에 기록됩니다. 커널 내부에서는 struct uid_gid_map으로 관리되며, 매핑은 한 번만 쓸 수 있고 이후 변경이 불가능합니다.
/* include/linux/user_namespace.h */
struct uid_gid_extent {
u32 first; /* 이 NS 내부의 시작 ID */
u32 lower_first; /* 부모 NS에서의 시작 ID */
u32 count; /* 매핑 범위 크기 */
};
struct uid_gid_map {
u32 nr_extents; /* 매핑 엔트리 수 */
union {
struct uid_gid_extent extent[5]; /* 인라인: 5개까지 */
struct {
struct uid_gid_extent *forward; /* 6개 이상 시 동적 배열 */
struct uid_gid_extent *reverse;
};
};
};
/*
* 매핑 규칙:
* 1. uid_map은 User NS 당 한 번만 쓸 수 있다
* 2. gid_map 쓰기 전에 /proc/PID/setgroups에 "deny" 기록 필요 (비특권 시)
* 3. 매핑되지 않은 UID는 overflow_uid(65534, nobody)로 표시
* 4. 최대 340개 매핑 엔트리 지원 (커널 5.12+)
*/
# User Namespace 생성 및 UID/GID 매핑 설정 전체 흐름
# 1. User NS 생성 (자식 프로세스)
unshare --user /bin/bash &
CHILD_PID=$!
# 2. setgroups deny (gid_map 쓰기 전 필수)
echo deny > /proc/$CHILD_PID/setgroups
# 3. UID 매핑: 컨테이너 UID 0 → 호스트 UID 1000, 범위 65536
echo "0 1000 65536" > /proc/$CHILD_PID/uid_map
# 4. GID 매핑: 동일 패턴
echo "0 1000 65536" > /proc/$CHILD_PID/gid_map
# 5. 확인
nsenter --user --target $CHILD_PID id
# uid=0(root) gid=0(root) groups=0(root)
# 6. 매핑되지 않은 UID는 nobody(65534)로 표시
nsenter --user --target $CHILD_PID cat /proc/self/uid_map
# 0 1000 65536
비특권 사용자가 User NS를 생성한 경우, gid_map을 쓰기 전에 반드시 /proc/PID/setgroups에 "deny"를 기록해야 합니다. 이는 setgroups() 시스템 콜을 통한 보조 그룹 변경으로 권한 상승을 방지하기 위한 보안 조치입니다. deny를 쓰면 해당 User NS 내에서는 setgroups()가 항상 -EPERM을 반환합니다.
subuid/subgid 설정
Rootless 컨테이너는 /etc/subuid와 /etc/subgid에 정의된 부가 UID/GID 범위를 사용합니다. 이 설정이 없으면 User NS 내에서 UID 매핑을 생성할 수 없습니다.
# /etc/subuid 형식: 사용자:시작UID:범위크기
cat /etc/subuid
# user1:100000:65536
# user2:165536:65536
# /etc/subgid 형식: 동일
cat /etc/subgid
# user1:100000:65536
# user2:165536:65536
# 새 사용자에게 subuid 범위 할당
usermod --add-subuids 200000-265535 newuser
usermod --add-subgids 200000-265535 newuser
# 또는 수동으로 편집
echo "newuser:200000:65536" >> /etc/subuid
echo "newuser:200000:65536" >> /etc/subgid
# 매핑 적용 확인
# 컨테이너 UID 0 → 호스트 UID 100000
# 컨테이너 UID 1 → 호스트 UID 100001
# 컨테이너 UID 1000 → 호스트 UID 101000
# 컨테이너 UID 65535 → 호스트 UID 165535
# 주의: 여러 컨테이너가 같은 subuid 범위를 사용하면
# 컨테이너 간 파일 공유 시 UID가 일치할 수 있음
# → 보안상 컨테이너별 별도 범위 사용 권장
idmapped mount (5.12+)
커널 5.12에서 도입된 idmapped mount는 User NS의 UID 매핑 문제를 마운트 레벨에서 해결합니다. 파일시스템 자체를 변경하지 않고, 마운트 시 UID/GID를 변환하는 방식입니다.
# idmapped mount 사용 예시 (mount_setattr syscall)
# 호스트의 /data (UID 1000 소유)를
# 컨테이너에서 UID 0으로 보이게 마운트
# mount-idmapped 유틸리티 사용 (linux-test-project)
mount-idmapped --map-mount b:0:1000:1 /data /mnt/container-data
# 결과:
# /data에서: -rw-r--r-- 1 user1(1000) group1 ... file.txt
# /mnt/container-data에서: -rw-r--r-- 1 root(0) root ... file.txt
# 장점:
# - 파일시스템 자체를 chown할 필요 없음
# - shiftfs보다 커널 네이티브 (upstream 지원)
# - 여러 컨테이너가 같은 볼륨을 다른 UID로 마운트 가능
# unprivileged 컨테이너 생성
unshare --user --map-root-user /bin/bash
id # uid=0(root) gid=0(root) (컨테이너 내부에서)
# UID 매핑 확인
cat /proc/self/uid_map
# 0 1000 1
Cgroup Namespace
Cgroup namespace는 프로세스의 cgroup 뷰를 격리합니다. 컨테이너 내에서 /proc/self/cgroup이 루트(/)로 보입니다. 이는 컨테이너가 자기가 호스트의 어떤 cgroup 경로에 배치되었는지를 알 수 없게 합니다.
/* kernel/cgroup/namespace.c */
struct cgroup_namespace {
refcount_t count;
struct ns_common ns;
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct css_set *root_cset; /* 이 NS의 루트 css_set */
};
/*
* cgroup NS 격리 원리:
* /proc/self/cgroup 읽기 시 현재 cgroup 경로에서
* cgroupns_root를 빼서 상대 경로를 표시합니다.
*
* 예: 호스트에서 /sys/fs/cgroup/system.slice/docker-abc.scope
* 컨테이너에서는 / 로 보임
*/
# cgroup namespace 격리 확인
# 호스트에서 컨테이너의 cgroup 경로 확인
cat /proc/$CONTAINER_PID/cgroup
# 0::/system.slice/docker-abc123.scope
# 컨테이너 내부에서 같은 정보 확인
nsenter --cgroup --target $CONTAINER_PID cat /proc/self/cgroup
# 0::/
# cgroup namespace 생성
unshare --cgroup /bin/bash
cat /proc/self/cgroup
# 0::/ (현재 위치가 루트로 보임)
Cgroup NS의 보안 함의
Cgroup namespace가 없으면 컨테이너 내부에서 /proc/self/cgroup을 읽을 때 호스트의 전체 cgroup 경로가 노출됩니다. 이 경로에는 Docker 컨테이너 ID, Kubernetes Pod 이름, systemd slice 이름 등이 포함되어 호스트 환경 정보를 유출할 수 있습니다.
# Cgroup NS가 없는 경우 (정보 누출)
cat /proc/self/cgroup
# 0::/system.slice/docker-abc123def456.scope
# → 호스트의 cgroup 경로, 컨테이너 ID 노출
# Cgroup NS가 있는 경우 (격리)
cat /proc/self/cgroup
# 0::/
# → 자신의 cgroup이 루트로 보임, 호스트 경로 숨겨짐
# cgroupfs 마운트도 Cgroup NS에 바인딩됨
ls /sys/fs/cgroup/
# 컨테이너 자신의 cgroup 하위만 보임
# Cgroup NS 내에서 새 하위 cgroup 생성
mkdir /sys/fs/cgroup/my-subgroup
echo $$ > /sys/fs/cgroup/my-subgroup/cgroup.procs
# 컨테이너 내부에서 자기 하위 cgroup 관리 가능
Cgroup NS와 Cgroup 컨트롤러의 차이: Cgroup namespace는 가시성만 격리합니다. 실제 리소스 제한은 cgroups 컨트롤러(cpu, memory, io 등)가 담당합니다. Cgroup NS는 컨테이너가 자신의 cgroup 루트를 /로 보게 하여 경로 누출을 방지하는 역할입니다. 따라서 Cgroup NS는 "보안을 위한 가시성 격리"이고, cgroup 컨트롤러는 "리소스 제한"입니다.
UTS와 IPC Namespace
UTS Namespace 상세
UTS namespace는 uname()이 반환하는 호스트명(nodename)과 NIS 도메인명(domainname)을 격리합니다. 구현이 단순하지만, 컨테이너 환경에서 호스트명 기반 서비스 디스커버리나 로깅에 직접 영향을 미칩니다.
/* include/linux/utsname.h */
struct uts_namespace {
struct new_utsname name; /* sysname, nodename, release, ... */
struct user_namespace *user_ns; /* 소유 User NS */
struct ucounts *ucounts;
struct ns_common ns;
refcount_t count;
};
struct new_utsname {
char sysname[65]; /* "Linux" */
char nodename[65]; /* hostname */
char release[65]; /* "6.8.0" */
char version[65]; /* "#1 SMP ..." */
char machine[65]; /* "x86_64" */
char domainname[65]; /* NIS 도메인 */
};
/* sethostname() → UTS NS의 nodename만 변경 */
SYSCALL_DEFINE2(sethostname, char __user *, name, int, len)
{
struct new_utsname *u;
/* 현재 UTS NS의 user_ns에 대해 CAP_SYS_ADMIN 검사 */
if (!ns_capable(current->nsproxy->uts_ns->user_ns, CAP_SYS_ADMIN))
return -EPERM;
/* ... */
}
UTS NS 운영 시 주의사항
# UTS NS에서 격리되는 것과 되지 않는 것
# 격리됨: hostname, domainname
unshare --uts /bin/bash
hostname my-container
hostname
# my-container (호스트에 영향 없음)
# 격리 안 됨: kernel release, version, machine
uname -r
# 6.1.141 (호스트와 동일 — 같은 커널 사용)
uname -m
# x86_64 (호스트와 동일)
# 주의: 일부 프로그램이 hostname에 의존하는 경우
# - DNS 역방향 조회 실패
# - 로그 수집기(hostname 기반 구분)
# - Oracle DB, SAP 등 라이선스 체크
# - Kerberos 인증 (FQDN 기반)
IPC Namespace 상세
IPC namespace는 System V IPC 객체(공유 메모리(Shared Memory), 세마포어(Semaphore), 메시지 큐(Message Queue))와 POSIX 메시지 큐를 격리합니다. 각 IPC NS는 독립적인 IPC ID 공간을 가지므로, 컨테이너 간에 IPC ID가 충돌하지 않습니다.
/* ipc/namespace.c */
struct ipc_namespace {
refcount_t count;
struct ipc_ids ids[3]; /* SHM, SEM, MSG 각각의 ID 테이블 */
int sem_ctls[4]; /* SEMMSL, SEMMNS, SEMOPM, SEMMNI */
int msg_ctlmax; /* MSGMAX */
int msg_ctlmnb; /* MSGMNB */
int msg_ctlmni; /* MSGMNI */
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct ns_common ns;
/* POSIX message queue 파일시스템 마운트 정보 */
struct mq_attr mq_queues_max;
unsigned int mq_msg_max;
unsigned int mq_msgsize_max;
unsigned int mq_msg_default;
unsigned int mq_msgsize_default;
};
/*
* 핵심: IPC NS가 새로 생성되면 IPC 객체 테이블이 비어 있습니다.
* 부모 NS의 IPC 객체를 복사하지 않으므로,
* 컨테이너 내에서 ipcs 명령은 빈 결과를 반환합니다.
*/
# UTS namespace: 호스트네임 격리
unshare --uts /bin/bash
hostname container-host
hostname # container-host (호스트에는 영향 없음)
# IPC namespace: System V IPC, POSIX 메시지 큐 격리
unshare --ipc /bin/bash
ipcs # 빈 IPC 테이블 (호스트의 IPC 객체 안보임)
# IPC NS 내에서 공유 메모리 생성
ipcmk -M 1024
ipcs -m # 방금 생성한 SHM만 보임
# 호스트에서 확인
ipcs -m # 컨테이너의 SHM은 보이지 않음
IPC NS별 리소스 제한
각 IPC namespace는 독립적인 SysV IPC 제한값을 가집니다. 이 값들은 /proc/sys/kernel/ 하위의 sysctl 파라미터로 조정합니다. IPC NS가 분리되면 각 NS 내에서 독립적으로 제한이 적용됩니다.
| 파라미터 | IPC 유형 | 설명 | 기본값 (일반적) |
|---|---|---|---|
shmmax | 공유 메모리 | 단일 세그먼트 최대 크기 (바이트) | 18446744073692774399 |
shmall | 공유 메모리 | 전체 공유 메모리 페이지 수 | 18446744073692774399 |
shmmni | 공유 메모리 | 최대 세그먼트 수 | 4096 |
msgmax | 메시지 큐 | 단일 메시지 최대 크기 (바이트) | 8192 |
msgmni | 메시지 큐 | 최대 큐 수 | 32000 |
sem | 세마포어 | SEMMSL SEMMNS SEMOPM SEMMNI | 32000 1024000000 500 32000 |
# IPC NS 내에서 제한값 확인 및 조정
unshare --ipc /bin/bash
# 현재 제한값 확인
cat /proc/sys/kernel/shmmax
cat /proc/sys/kernel/msgmni
cat /proc/sys/kernel/sem
# IPC NS별 POSIX 메시지 큐 제한
cat /proc/sys/fs/mqueue/msg_max
cat /proc/sys/fs/mqueue/queues_max
네임스페이스 생명주기와 관리
생성, 공유, 해제 흐름
네임스페이스의 수명은 참조 카운팅(Reference Counting)으로 관리됩니다. 네임스페이스를 참조하는 것은 프로세스뿐만 아니라 열린 파일 디스크립터(File Descriptor), bind mount, nsfs 파일 등이 있습니다. 모든 참조가 해제되어야 네임스페이스가 실제로 소멸합니다.
| 참조 소스 | 참조 추가 시점 | 참조 해제 시점 | 운영 주의사항 |
|---|---|---|---|
프로세스 (task_struct) | clone(), setns() | 프로세스 종료 (exit()) | 가장 일반적인 참조 소스 |
| 열린 fd | open("/proc/PID/ns/...") | close(fd) | fd 누수 시 네임스페이스 잔류 |
| bind mount | mount --bind /proc/PID/ns/net /run/... | umount() | CNI 플러그인이 자주 사용 |
| 자식 네임스페이스 | User NS 계층 구조 | 자식 NS 소멸 | User NS는 자식이 있으면 유지됨 |
| 소유된 non-user NS | NS 생성 시 user_ns 포인터 | 소유된 NS 소멸 | User NS 해제는 소유 NS 해제 후 |
/* 네임스페이스 생성 방법 3가지 */
/* 1. clone() 시 플래그로 생성 */
clone(child_fn, stack, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, arg);
/* 2. unshare()로 현재 프로세스의 namespace 분리 */
unshare(CLONE_NEWNS | CLONE_NEWPID);
/* 3. setns()로 기존 namespace에 진입 */
int fd = open("/proc/PID/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);
close(fd);
User Namespace를 활용한 비특권 컨테이너 C 예제
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>
#define STACK_SIZE (1024 * 1024)
static void write_file(const char *path, const char *content)
{
int fd = open(path, O_WRONLY);
if (fd == -1) { perror(path); return; }
write(fd, content, strlen(content));
close(fd);
}
static int child_fn(void *arg)
{
/* 이 시점에서는 새 User NS 내부 */
/* UID/GID 매핑이 설정될 때까지 대기 */
sleep(1);
printf("[child] uid=%d gid=%d\n", getuid(), getgid());
printf("[child] euid=%d egid=%d\n", geteuid(), getegid());
/* 새 UTS NS에서 호스트명 변경 (User NS 덕분에 가능) */
sethostname("rootless-container", 20);
char hostname[64];
gethostname(hostname, sizeof(hostname));
printf("[child] hostname=%s\n", hostname);
execl("/bin/sh", "sh", NULL);
return 1;
}
int main(void)
{
char *stack = malloc(STACK_SIZE);
char map_buf[64], path_buf[64];
printf("[parent] uid=%d, creating unprivileged container...\n", getuid());
/* User NS + UTS NS + Mount NS를 비특권 사용자로 생성 */
pid_t child = clone(child_fn, stack + STACK_SIZE,
CLONE_NEWUSER | CLONE_NEWUTS | CLONE_NEWNS | SIGCHLD,
NULL);
if (child == -1) {
perror("clone");
return 1;
}
/* 부모에서 자식의 UID/GID 매핑 설정 */
/* setgroups deny (보안 요구사항) */
snprintf(path_buf, sizeof(path_buf), "/proc/%d/setgroups", child);
write_file(path_buf, "deny");
/* UID 매핑: 컨테이너 UID 0 → 현재 사용자 UID */
snprintf(path_buf, sizeof(path_buf), "/proc/%d/uid_map", child);
snprintf(map_buf, sizeof(map_buf), "0 %d 1\n", getuid());
write_file(path_buf, map_buf);
/* GID 매핑: 컨테이너 GID 0 → 현재 사용자 GID */
snprintf(path_buf, sizeof(path_buf), "/proc/%d/gid_map", child);
snprintf(map_buf, sizeof(map_buf), "0 %d 1\n", getgid());
write_file(path_buf, map_buf);
printf("[parent] UID/GID mapping configured for child %d\n", child);
waitpid(child, NULL, 0);
free(stack);
return 0;
}
/* 빌드 및 실행 (root 권한 불필요!):
* gcc -o rootless-ns rootless-ns.c
* ./rootless-ns
* [parent] uid=1000, creating unprivileged container...
* [parent] UID/GID mapping configured for child 12345
* [child] uid=0 gid=0 ← 컨테이너 내에서 root!
* [child] hostname=rootless-container
* $ ← 셸 진입 (User NS 내부)
*/
# 프로세스의 네임스페이스 확인
ls -la /proc/self/ns/
# cgroup -> cgroup:[4026531835]
# ipc -> ipc:[4026531839]
# mnt -> mnt:[4026531841]
# net -> net:[4026531840]
# pid -> pid:[4026531836]
# user -> user:[4026531837]
# uts -> uts:[4026531838]
# 네임스페이스를 파일로 유지 (bind mount)
touch /run/netns/my_netns
mount --bind /proc/self/ns/net /run/netns/my_netns
# ip netns는 이 방식 사용
# nsenter: 기존 네임스페이스에 진입
nsenter --target $PID --net --pid --mount /bin/bash
pidfd와 네임스페이스 통합
전통적인 PID 기반 프로세스 제어는 PID 재사용 경쟁(TOCTOU) 문제가 있습니다. 프로세스가 종료된 후 같은 PID가 다른 프로세스에 할당되면, kill()이나 waitpid()가 엉뚱한 대상에 작용할 수 있습니다. 커널 5.3에서 도입된 pidfd는 이 문제를 해결하며, 네임스페이스 진입에도 활용됩니다.
#include <sys/syscall.h>
#include <linux/sched.h>
/* 1. pidfd_open(): PID를 파일 디스크립터로 변환 (5.3+) */
int pidfd = syscall(SYS_pidfd_open, target_pid, 0);
if (pidfd < 0) perror("pidfd_open");
/* 2. pidfd로 시그널 전송: PID 재사용 문제 없음 (5.1+) */
syscall(SYS_pidfd_send_signal, pidfd, SIGTERM, NULL, 0);
/* 3. pidfd + setns(): 여러 namespace를 한 번에 진입 (5.8+) */
/* pidfd에서 PID, net, mnt namespace를 동시 진입 */
setns(pidfd, CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS);
/* 4. clone3()에서 pidfd 자동 생성 (5.2+) */
struct clone_args args = {
.flags = CLONE_PIDFD | CLONE_NEWPID | CLONE_NEWNET,
.pidfd = (__u64)&pidfd,
.exit_signal = SIGCHLD,
};
pid_t child = syscall(SYS_clone3, &args, sizeof(args));
/* child 생성 + 새 PID/Net NS + pidfd 한 번에 획득 */
/* 5. pidfd_getfd(): 다른 프로세스의 fd 복제 (5.6+) */
int remote_fd = syscall(SYS_pidfd_getfd, pidfd, target_fd, 0);
/* namespace 경계를 넘어 fd를 안전하게 복제 */
pidfd + setns 조합의 장점: 기존 방식은 /proc/<pid>/ns/net, /proc/<pid>/ns/mnt 등을 각각 열어서 setns()를 여러 번 호출해야 했습니다. pidfd 방식은 하나의 fd로 여러 namespace를 원자적(Atomic)으로 진입할 수 있어, 중간 상태(일부 namespace만 전환된 불일치)가 발생하지 않습니다.
| 기능 | 커널 버전 | 시스템 콜 | 네임스페이스 관련 용도 |
|---|---|---|---|
| pidfd 시그널(Signal) 전송 | 5.1 | pidfd_send_signal() | PID NS 경계에서 안전한 시그널 |
| clone3 + CLONE_PIDFD | 5.2 | clone3() | NS 생성 + pidfd 동시 획득 |
| pidfd_open | 5.3 | pidfd_open() | 기존 프로세스의 pidfd 획득 |
| pidfd_getfd | 5.6 | pidfd_getfd() | NS 경계 넘어 fd 복제 |
| setns + pidfd | 5.8 | setns(pidfd, flags) | 한 번에 여러 NS 원자 진입 |
| waitid + P_PIDFD | 5.4 | waitid() | pidfd로 자식 대기 |
실전 디버깅: nsenter와 lsns
운영 환경에서 네임스페이스 문제를 진단할 때 가장 많이 사용하는 도구는 nsenter와 lsns입니다. 각 도구의 정확한 사용법과 자주 발생하는 함정을 정리합니다.
lsns: 시스템 네임스페이스 전체 조회
# 시스템의 모든 네임스페이스 조회
lsns
# NS TYPE NPROCS PID USER COMMAND
# 4026531835 cgroup 143 1 root /sbin/init
# 4026531836 pid 143 1 root /sbin/init
# 4026531837 user 143 1 root /sbin/init
# 4026531838 uts 143 1 root /sbin/init
# 4026531839 ipc 143 1 root /sbin/init
# 4026531840 net 143 1 root /sbin/init
# 4026531841 mnt 132 1 root /sbin/init
# 4026532200 mnt 1 1234 root /usr/bin/containerd-shim
# 4026532201 pid 3 1235 root /sbin/init (container)
# 특정 유형만 필터링
lsns -t net
lsns -t pid
# JSON 출력 (스크립트용)
lsns -J -t net
# 특정 프로세스의 namespace 확인
lsns -p $PID
# 두 프로세스가 같은 namespace를 공유하는지 확인
readlink /proc/$PID1/ns/net
readlink /proc/$PID2/ns/net
# inode 번호가 같으면 같은 namespace
nsenter: 네임스페이스 진입
# 컨테이너의 모든 namespace에 진입
nsenter --target $PID --all /bin/bash
# 특정 namespace만 선택적으로 진입
nsenter --target $PID --net ip addr show
nsenter --target $PID --net --pid --mount /bin/bash
# bind mount된 namespace로 진입
nsenter --net=/run/netns/my-container ip route show
# Docker 컨테이너 디버깅
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' my-container)
nsenter --target $CONTAINER_PID --net ss -tlnp
nsenter --target $CONTAINER_PID --net iptables -L -n
# mount namespace 진입 시 주의: root/pwd가 바뀔 수 있음
nsenter --target $PID --mount ls /
# 컨테이너의 루트 파일시스템이 보임 (호스트 파일시스템 아님)
nsenter가 실패하면 다음을 확인하세요:
(1) 권한: CAP_SYS_ADMIN 또는 CAP_SYS_PTRACE가 필요합니다.
(2) 프로세스 존재: 대상 PID가 이미 종료되었을 수 있습니다. kill -0 $PID로 확인하세요.
(3) AppArmor/SELinux: 보안 모듈이 namespace 진입을 차단할 수 있습니다.
(4) ptrace 제한: /proc/sys/kernel/yama/ptrace_scope가 1 이상이면 제한됩니다.
(5) User NS 경계: 다른 User NS의 프로세스에 진입하려면 추가 권한이 필요합니다.
procfs를 통한 namespace 심층 조사
# 프로세스의 전체 namespace 목록
ls -la /proc/$PID/ns/
# cgroup -> cgroup:[4026531835]
# ipc -> ipc:[4026531839]
# mnt -> mnt:[4026532200]
# net -> net:[4026532205]
# pid -> pid:[4026532201]
# pid_for_children -> pid:[4026532201]
# time -> time:[4026531834]
# time_for_children -> time:[4026531834]
# user -> user:[4026531837]
# uts -> uts:[4026532199]
# User NS 계층 확인: /proc/PID/status
grep -i ns /proc/$PID/status
# NStgid: 1234 1 (호스트 PID, 컨테이너 PID)
# NSpid: 1234 1
# NSpgid: 1234 1
# NSsid: 1234 1
# UID 매핑 확인
cat /proc/$PID/uid_map
# 0 1000 65536
cat /proc/$PID/gid_map
# User NS 소유자 확인 (ioctl)
# python3 -c "import fcntl,os,struct; ..."
# 컨테이너 내부 PID 1과 호스트 PID 대응 확인
cat /proc/$PID/status | grep NSpid
# NSpid: 45678 1 ← 호스트에서 45678, 컨테이너에서 1
컨테이너 런타임과의 관계
Docker, Podman 등의 컨테이너 런타임은 namespace를 조합하여 격리된 환경을 구성합니다. 런타임은 보통 runc(또는 crun) 같은 OCI 런타임을 호출하며, 이 런타임이 실제로 clone3() / unshare()를 사용하여 namespace를 생성합니다.
OCI Runtime Spec의 네임스페이스 설정
OCI(Open Container Initiative) 런타임 스펙은 config.json에서 네임스페이스 설정을 정의합니다. runc, crun, youki 등의 OCI 호환 런타임은 이 설정에 따라 네임스페이스를 생성하거나 기존 네임스페이스에 진입합니다.
{
"linux": {
"namespaces": [
{ "type": "pid" },
{ "type": "network" },
{ "type": "ipc" },
{ "type": "uts" },
{ "type": "mount" },
{ "type": "cgroup" },
{
"type": "user",
"path": "/proc/1234/ns/user"
/* path 지정 시 기존 NS에 진입, 미지정 시 새 NS 생성 */
}
],
"uidMappings": [
{ "containerID": 0, "hostID": 100000, "size": 65536 }
],
"gidMappings": [
{ "containerID": 0, "hostID": 100000, "size": 65536 }
]
}
}
Docker vs Podman의 네임스페이스 차이
| 특성 | Docker (dockerd + runc) | Podman (crun/runc) |
|---|---|---|
| 데몬(Daemon) | dockerd 데몬 필요 (root) | 데몬 없음 (daemonless) |
| User NS (기본) | 비활성화 (root 실행) | rootless 시 자동 활성화 |
| UID 매핑 | --userns-remap 옵션 | /etc/subuid 자동 사용 |
| Network NS | docker0 bridge + veth | slirp4netns 또는 pasta |
| PID NS init | tini (docker-init) | conmon (container monitor) |
| 보안 강화 | seccomp + AppArmor | seccomp + SELinux |
| cgroup 드라이버 | cgroupfs 또는 systemd | systemd 기본 |
Rootless 컨테이너의 네임스페이스 구성
Rootless 컨테이너는 비특권 사용자가 User NS를 기반으로 다른 네임스페이스를 생성하는 방식입니다. 이 구성에서는 호스트 권한 없이도 컨테이너를 실행할 수 있지만, 네트워크 설정과 마운트에 제약이 있습니다.
# Rootless 컨테이너의 네임스페이스 구성 확인
# Podman rootless 컨테이너 실행
podman run -d --name test alpine sleep infinity
# 컨테이너 프로세스의 User NS 확인
podman inspect test --format '{{.State.Pid}}'
CONTAINER_PID=$(podman inspect test --format '{{.State.Pid}}')
# User NS가 호스트와 다른 것을 확인
readlink /proc/self/ns/user
# user:[4026531837]
readlink /proc/$CONTAINER_PID/ns/user
# user:[4026532500] ← 다른 User NS
# UID 매핑 확인
cat /proc/$CONTAINER_PID/uid_map
# 0 100000 65536
# 컨테이너 UID 0 = 호스트 UID 100000
# Rootless 네트워크: slirp4netns 확인
ps aux | grep slirp4netns
# slirp4netns가 TAP 인터페이스로 네트워크 제공
# Rootless에서의 제약사항:
# - 1024 미만 포트 바인딩 불가 (CAP_NET_BIND_SERVICE 없음)
# - ping 제한 (CAP_NET_RAW 없음, sysctl 설정 필요)
# - 일부 파일시스템(NFS, FUSE) 마운트 제한
# - /etc/subuid, /etc/subgid 설정 필수
| Namespace | 격리 대상 | 커널 버전 | clone 플래그 |
|---|---|---|---|
| Mount | 파일시스템 마운트 포인트 | 2.4.19 | CLONE_NEWNS |
| UTS | 호스트네임, 도메인네임 | 2.6.19 | CLONE_NEWUTS |
| IPC | System V IPC, POSIX MQ | 2.6.19 | CLONE_NEWIPC |
| PID | 프로세스 ID | 2.6.24 | CLONE_NEWPID |
| Network | 네트워크 스택 | 2.6.29 | CLONE_NEWNET |
| User | UID/GID 매핑 | 3.8 | CLONE_NEWUSER |
| Cgroup | cgroup 루트 디렉토리 | 4.6 | CLONE_NEWCGROUP |
| Time | 부트 시간, 모노토닉 시계 | 5.6 | CLONE_NEWTIME |
참고 자료: LWN: Namespaces in operation 시리즈, man 7 namespaces, man 7 pid_namespaces, man 7 network_namespaces
네임스페이스 보안 강화
네임스페이스는 격리를 제공하지만 보안 경계는 아닙니다. 커널 공유 자원에 대한 접근은 여전히 가능하며, 특히 User NS는 새로운 공격 면적을 열 수 있습니다. 운영 환경에서는 namespace + seccomp + capabilities + LSM(AppArmor/SELinux)을 조합해야 합니다. 이 절에서는 네임스페이스 관련 보안 위협과 방어 패턴을 체계적으로 정리합니다.
다층 방어 (Defense in Depth) 모델
seccomp과 네임스페이스 연동
seccomp(Secure Computing Mode)는 프로세스가 호출할 수 있는 시스템 콜을 제한합니다. 네임스페이스 관련 위험한 시스템 콜을 차단하여 공격면을 줄입니다.
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <sys/prctl.h>
/* 컨테이너에서 차단해야 할 네임스페이스 관련 syscall */
/*
* - mount / umount2: 파일시스템 조작으로 탈출 가능
* - unshare: 새 네임스페이스 생성으로 권한 우회 가능
* - clone (CLONE_NEW* 플래그): 새 네임스페이스 생성
* - pivot_root: 루트 파일시스템 변경
* - ptrace: 다른 프로세스 디버깅으로 정보 유출
* - keyctl: 키링 조작
* - add_key / request_key: 커널 키 관리
*
* Docker 기본 seccomp 프로파일은 약 50개의 syscall을 차단합니다.
* 가장 중요한 차단 대상은 위의 네임스페이스 관련 syscall들입니다.
*/
/* Docker 기본 seccomp 프로파일 중 네임스페이스 관련 발췌 */
/*
* {
* "names": ["unshare"],
* "action": "SCMP_ACT_ALLOW",
* "args": [],
* "comment": "User NS가 비활성화되어 있으므로 unshare 허용"
* },
* {
* "names": ["clone"],
* "action": "SCMP_ACT_ALLOW",
* "args": [
* { "index": 0, "value": 2114060288, "op": "SCMP_CMP_MASKED_EQ" }
* ],
* "comment": "CLONE_NEWUSER 비트가 설정되면 차단"
* }
*/
# Docker seccomp 프로파일 확인
docker run --rm alpine grep Seccomp /proc/self/status
# Seccomp: 2 (mode 2 = BPF 필터 활성)
# Seccomp_filters: 1
# seccomp 비활성화 (위험! 디버깅 목적만)
docker run --security-opt seccomp=unconfined alpine sh
# 커스텀 seccomp 프로파일 적용
docker run --security-opt seccomp=my-profile.json alpine sh
namespace 관련 sysctl 파라미터
| sysctl 파라미터 | 기본값 | 권장값 | 설명 |
|---|---|---|---|
user.max_user_namespaces | 무제한 | 0 또는 제한 | User NS 생성 수 제한. 0이면 완전 비활성화 |
kernel.unprivileged_userns_clone | 1 (Debian/Ubuntu) | 0 | 비특권 User NS 생성 차단 |
kernel.unprivileged_bpf_disabled | 2 | 2 | User NS 내 BPF 프로그램 로드 차단 |
kernel.perf_event_paranoid | 2 | 3 | User NS 내 perf 이벤트 제한 |
user.max_pid_namespaces | 무제한 | 환경별 설정 | PID NS 최대 수 |
user.max_net_namespaces | 무제한 | 환경별 설정 | Net NS 최대 수 |
user.max_mnt_namespaces | 무제한 | 환경별 설정 | Mount NS 최대 수 |
Capabilities 최소화 패턴
# Docker에서 모든 capability를 제거하고 필요한 것만 추가
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE --cap-add=CHOWN nginx
# 현재 프로세스의 capability 확인
cat /proc/self/status | grep Cap
# CapInh: 0000000000000000
# CapPrm: 000001ffffffffff
# CapEff: 000001ffffffffff
# CapBnd: 000001ffffffffff
# CapAmb: 0000000000000000
# capability 비트 해석
capsh --decode=000001ffffffffff
# User NS 내부의 capability: 전체 capability가 있지만
# 해당 User NS가 소유하는 자원에만 적용됩니다
unshare --user --map-root-user capsh --print
# Current: =ep (전체 capability)
# 하지만 호스트 자원에 대해서는 무력
(1) 커널 공유: 모든 namespace는 같은 커널 위에서 실행됩니다. 커널 취약점(Vulnerability)은 namespace 경계를 무시합니다.
(2) procfs/sysfs 누출: /proc, /sys의 일부 파일은 namespace 격리가 불완전합니다.
(3) 디바이스 접근: /dev 장치 파일은 기본적으로 Mount NS만 격리합니다. cgroup device controller 또는 seccomp가 추가로 필요합니다.
(4) User NS 공격면: User NS는 비특권 사용자에게 커널의 많은 코드 경로를 노출합니다.
네임스페이스 관련 주요 취약점
User namespace는 비특권 사용자에게 네임스페이스 내부의 CAP_SYS_ADMIN을 부여하므로, 커널의 다양한 공격 면적을 노출합니다. 많은 커널 취약점이 user namespace를 전제 조건으로 요구하며, 이는 namespace 자체의 보안 설계와 밀접하게 연관됩니다.
fsconfig() 시스템 콜의 legacy_parse_param()에서 파라미터 길이 검증 부족으로 정수 언더플로우가 발생합니다. User namespace 내의 CAP_SYS_ADMIN만으로 트리거 가능하며, 힙 오버플로우를 통해 init namespace로 탈출할 수 있습니다. Linux 5.1~5.16에 영향을 미칩니다.
Ubuntu 커널에 적용된 OverlayFS 패치(Patch)에서, user namespace 내에서 OverlayFS를 마운트할 때 trusted.overlayfs.metacopy xattr 설정이 권한 검사를 우회합니다. 이를 통해 비특권 사용자가 임의 파일에 setuid/setcap 속성을 설정하여 root 권한을 획득할 수 있습니다.
/* User Namespace가 공격 전제 조건인 주요 CVE 목록 */
/*
* CVE-2022-0185: fsconfig() 힙 오버플로우
* → user NS의 CAP_SYS_ADMIN으로 fsconfig() 호출 가능
*
* CVE-2022-0492: cgroup v1 release_agent
* → user NS + cgroup NS로 cgroup 마운트 가능
*
* CVE-2022-1015: nf_tables 스택 버퍼 오버플로우
* → user NS의 CAP_NET_ADMIN으로 nf_tables 규칙 생성
*
* CVE-2023-0386: OverlayFS + FUSE setuid 우회
* → user NS에서 FUSE + OverlayFS 마운트 가능
*
* CVE-2023-32233: nf_tables 익명 set UAF
* → user NS의 CAP_NET_ADMIN으로 nf_tables 조작
*
* CVE-2024-1086: nf_tables verdict UAF
* → user NS의 CAP_NET_ADMIN으로 트리거
*/
/* User Namespace 공격 면적 제한 방법 */
# 방법 1: User NS 완전 비활성화 (가장 강력)
sysctl -w user.max_user_namespaces=0
# 방법 2: 비특권 User NS 비활성화 (Debian/Ubuntu)
sysctl -w kernel.unprivileged_userns_clone=0
# 방법 3: BPF와 perf 제한 (user NS 내 CAP_SYS_ADMIN 영향 축소)
sysctl -w kernel.unprivileged_bpf_disabled=2
sysctl -w kernel.perf_event_paranoid=3
# 방법 4: AppArmor로 user NS 내 특정 작업 제한 (Ubuntu 23.10+)
# /etc/apparmor.d/unprivileged_userns 프로파일 활용
PID 재사용 공격: PID namespace 내에서 프로세스가 종료되고 동일 PID가 재할당될 때, kill()이나 ptrace() 등의 시스템 콜이 의도하지 않은 프로세스에 작용할 수 있습니다. pidfd_open()(5.3+)을 사용하여 PID 대신 파일 디스크립터로 프로세스를 참조하면 이 문제를 방지할 수 있습니다.
프로세스 정보 누출: /proc 파일시스템의 hidepid=2 마운트 옵션을 사용하여 다른 사용자의 프로세스 정보를 숨길 수 있습니다.
네임스페이스 탈출 패턴과 방어
네임스페이스 탈출(Escape)은 컨테이너 내부에서 호스트 리소스에 접근하거나, 호스트 권한을 획득하는 것을 의미합니다. 주요 탈출 패턴과 방어 방법을 정리합니다.
| 탈출 패턴 | 필요 조건 | 방어 방법 |
|---|---|---|
| privileged 컨테이너 | --privileged 플래그 | 절대 사용하지 않음, seccomp/capabilities 사용 |
| 호스트 PID/Net NS 공유 | --pid=host, --net=host | 필요한 경우에만 최소 범위로 사용 |
| Docker 소켓 마운트 | -v /var/run/docker.sock:/var/run/docker.sock | Docker 소켓 접근 차단 |
/proc/sysrq-trigger | procfs 마운트 + 쓰기 권한 | /proc를 readonly 마운트 |
| cgroup release_agent | cgroup v1 + CAP_SYS_ADMIN | cgroup v2 사용, seccomp으로 차단 |
nsenter 남용 | 호스트 nsenter 바이너리 접근 | read-only rootfs, 불필요한 도구 제거 |
| 커널 모듈 로드 | CAP_SYS_MODULE | capability 제거, seccomp 차단 |
| 디바이스 파일 접근 | /dev 디바이스 노드 | device cgroup 컨트롤러 사용 |
# === 안전한 컨테이너 실행 모범 사례 ===
# 1. 모든 capability 제거 + 필요한 것만 추가
docker run --cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--cap-add=CHOWN \
--cap-add=SETUID \
--cap-add=SETGID \
nginx
# 2. 읽기 전용 루트 파일시스템
docker run --read-only \
--tmpfs /tmp:rw,noexec,nosuid \
--tmpfs /run:rw,noexec,nosuid \
nginx
# 3. 네트워크 제한
docker run --network=none alpine # 네트워크 완전 차단
# 4. seccomp 프로파일 적용
docker run --security-opt seccomp=default.json alpine
# 5. no-new-privileges (setuid 바이너리 무력화)
docker run --security-opt no-new-privileges alpine
# 6. /proc 보호
docker run --read-only \
-v /proc:/host-proc:ro \
alpine
# 7. Rootless 모드 (가장 강력한 방어)
podman run --userns=auto alpine
/proc, /sys를 통한 정보 누출 방어
컨테이너 내부의 /proc와 /sys는 호스트 커널의 정보를 노출할 수 있습니다. 컨테이너 런타임은 이를 방어하기 위해 여러 경로를 마스킹(masking)하거나 읽기 전용으로 마운트합니다.
# Docker/runc가 기본으로 마스킹하는 /proc 경로
# (tmpfs로 덮어씌워 내용을 숨김)
# /proc/asound - 사운드 디바이스 정보
# /proc/acpi - ACPI 하드웨어 정보
# /proc/kcore - 커널 메모리 !
# /proc/keys - 키링 정보
# /proc/latency_stats - 레이턴시 통계
# /proc/timer_list - 타이머 정보
# /proc/timer_stats - 타이머 통계
# /proc/sched_debug - 스케줄러 디버그
# /proc/scsi - SCSI 디바이스
# Docker/runc가 읽기 전용으로 마운트하는 경로
# /proc/bus - 버스 정보
# /proc/fs - 파일시스템 정보
# /proc/irq - 인터럽트 정보
# /proc/sys - sysctl 파라미터 !
# /proc/sysrq-trigger - SysRq 트리거 !
# /sys - sysfs 전체
# 컨테이너 내에서 확인
docker run --rm alpine mount | grep -E '(proc|sys)'
# proc on /proc type proc (rw,nosuid,nodev,noexec,...)
# tmpfs on /proc/asound type tmpfs (ro,...)
# tmpfs on /proc/kcore type tmpfs (ro,...)
# hidepid로 /proc 프로세스 정보 격리 강화
mount -o remount,hidepid=2 /proc
# 다른 사용자의 /proc/PID 디렉터리가 보이지 않음
네임스페이스 성능 고려사항
네임스페이스 격리는 가상 머신에 비해 오버헤드(Overhead)가 매우 작지만, 완전히 무료는 아닙니다. 운영 환경에서 네임스페이스 수가 많아지면 영향이 누적될 수 있습니다.
| 네임스페이스 | 생성 비용 | 런타임 오버헤드 | 주요 병목(Bottleneck) |
|---|---|---|---|
| PID NS | 낮음 | PID 번역 시 계층 탐색 | 깊은 중첩 시 struct pid 배열 크기 증가 |
| Mount NS | 높음 | 마운트 포인트 복사 | 마운트 포인트 수에 비례하는 메모리. 수만 개의 마운트가 있으면 unshare(CLONE_NEWNS)가 느려짐 |
| Net NS | 중간 | 네트워크 스택 초기화 | 대량의 Net NS 생성 시 네트워크 장치 인덱스 소진, loopback 생성 비용 |
| User NS | 낮음 | ns_capable() 체인 탐색 | 깊은 User NS 중첩 시 capability 판정 비용 증가 |
| UTS NS | 매우 낮음 | 거의 없음 | 문자열 복사만 (260바이트) |
| IPC NS | 낮음 | 거의 없음 | IPC 객체 테이블 초기화 |
| Cgroup NS | 매우 낮음 | 거의 없음 | css_set 참조만 |
| Time NS | 낮음 | vDSO 오프셋 적용 | vDSO 매핑 전환 비용 |
Mount NS 생성 최적화: 마운트 포인트가 수천 개인 호스트에서 unshare(CLONE_NEWNS)를 호출하면 전체 마운트 테이블을 복사하므로 수 밀리초에서 수십 밀리초가 걸릴 수 있습니다. 컨테이너 런타임은 보통 mount propagation을 slave나 private로 설정하여 불필요한 이벤트 전파를 줄입니다. findmnt | wc -l로 현재 마운트 수를 확인하세요.
네임스페이스 성능 벤치마크
# === 네임스페이스 생성 비용 측정 ===
# 1. UTS NS 생성 시간 (가장 가벼움)
time for i in $(seq 1 1000); do
unshare --uts true
done
# 약 0.5~1초 (1000회)
# 2. PID NS 생성 시간
time for i in $(seq 1 1000); do
unshare --pid --fork true
done
# 약 2~4초 (1000회, fork 포함)
# 3. Net NS 생성 시간 (loopback 초기화 포함)
time for i in $(seq 1 100); do
ip netns add test_$i
done
time for i in $(seq 1 100); do
ip netns delete test_$i
done
# 4. Mount NS 생성 시간 (마운트 포인트 수에 비례)
echo "현재 마운트 포인트 수: $(findmnt | wc -l)"
time unshare --mount true
# 마운트가 100개 미만이면 1ms 미만
# 마운트가 10000개 이상이면 수십 ms
# 5. 전체 컨테이너 namespace 조합 생성 시간
time unshare --pid --fork --mount --net --uts --ipc --cgroup true
# 약 2~5ms (일반적인 호스트에서)
# === 대량 네임스페이스 환경에서의 커널 리소스 사용 ===
# 네트워크 네임스페이스당 메모리 사용량 추정
# - struct net: 약 4~8KB
# - loopback 장치: 약 2KB
# - 라우팅 테이블: 약 1KB (초기)
# - netfilter 초기화: 약 4KB
# 합계: 약 10~15KB per net NS (초기 상태)
# 1000개 컨테이너 환경에서의 리소스 확인
cat /proc/meminfo | grep Slab
slabtop -o | head -20
# net_namespace, mnt_namespace, pid_namespace 등의
# slab 캐시 사용량을 확인
대규모 환경 최적화 전략
| 최적화 대상 | 문제 상황 | 해결 방법 |
|---|---|---|
| Mount NS 생성 지연 | 호스트에 마운트 포인트가 수천 개 | 불필요한 마운트 제거, --propagation private 사용 |
| Net NS 장치 인덱스 고갈 | 대량의 veth 생성/삭제 반복 | net.core.dev_weight 튜닝, 인덱스 재사용 대기 |
| PID NS 깊은 중첩 | struct pid 배열 크기 증가 | 중첩 깊이 제한 (보통 2~3레벨이면 충분) |
| User NS capability 체인 | ns_capable()의 부모 체인 탐색 | User NS 중첩 깊이 제한 (user.max_user_namespaces) |
| 좀비 네임스페이스 누적 | fd/bind mount 미해제 | 주기적 정리 스크립트, 모니터링 알람 |
| conntrack 테이블 공유 | Net NS간 conntrack 분리 | 각 Net NS에 독립 conntrack, nf_conntrack_max 조정 |
운영 트러블슈팅 패턴
네임스페이스 관련 운영 문제는 크게 세 가지로 분류됩니다: (1) 네임스페이스 누수(Leak) — 프로세스는 종료되었지만 네임스페이스가 남아 있는 경우, (2) 연결 실패 — 네임스페이스 간 통신이 끊어진 경우, (3) 권한 문제 — 네임스페이스 진입이나 조작이 실패하는 경우입니다.
네임스페이스 진단 워크플로우
# =========================================
# 네임스페이스 진단 통합 스크립트
# =========================================
# 1단계: 전체 네임스페이스 현황 파악
echo "=== 네임스페이스 현황 ==="
lsns --no-headings | awk '{print $2}' | sort | uniq -c | sort -rn
# 2단계: 특정 컨테이너의 네임스페이스 확인
TARGET_PID=1234
echo "=== PID $TARGET_PID 네임스페이스 ==="
ls -la /proc/$TARGET_PID/ns/
# 3단계: 두 프로세스가 같은 네임스페이스를 공유하는지 확인
echo "=== 네임스페이스 비교 ==="
for ns in pid net mnt uts ipc user cgroup; do
ns1=$(readlink /proc/$PID1/ns/$ns)
ns2=$(readlink /proc/$PID2/ns/$ns)
if [ "$ns1" = "$ns2" ]; then
echo " $ns: 공유 ($ns1)"
else
echo " $ns: 다름 ($ns1 vs $ns2)"
fi
done
# 4단계: PID NS 계층 관계 확인
echo "=== PID NS 계층 ==="
grep NSpid /proc/$TARGET_PID/status
# NSpid: 호스트PID 중간PID 컨테이너PID
# 5단계: 네트워크 네임스페이스 내 상태 확인
echo "=== 네트워크 NS 상태 ==="
nsenter --target $TARGET_PID --net ip addr show
nsenter --target $TARGET_PID --net ip route show
nsenter --target $TARGET_PID --net ss -tlnp
nsenter --target $TARGET_PID --net iptables -L -n -v
# 6단계: 마운트 네임스페이스 내 마운트 포인트 확인
echo "=== 마운트 NS 상태 ==="
nsenter --target $TARGET_PID --mount findmnt --tree
# 7단계: User NS UID 매핑 확인
echo "=== User NS 매핑 ==="
cat /proc/$TARGET_PID/uid_map
cat /proc/$TARGET_PID/gid_map
cat /proc/$TARGET_PID/status | grep -E '(Cap|Ns|Uid|Gid)'
좀비 네임스페이스 진단
"프로세스는 다 죽었는데 네임스페이스가 남아 있다"는 상황은 운영 환경에서 자주 발생합니다. 네임스페이스의 수명은 참조 카운트에 의존하므로, 프로세스 외에 fd나 bind mount가 참조를 잡고 있을 수 있습니다.
# 1. 소유 프로세스가 없는 네임스페이스 찾기
lsns | awk '$3 == 0 {print}'
# 2. 네임스페이스를 잡고 있는 bind mount 찾기
grep nsfs /proc/mounts
# nsfs /run/netns/my-container nsfs rw 0 0
# 3. 네임스페이스 fd를 열고 있는 프로세스 찾기
find /proc/*/fd -lname '*net:\[4026532205\]*' 2>/dev/null
# 4. 정리: bind mount 해제 → namespace 자동 소멸
umount /run/netns/my-container
# 5. CNI 플러그인이 남긴 네임스페이스 정리
ip netns list
ip netns delete old-container-ns
네임스페이스 누수 모니터링
# 네임스페이스 유형별 현재 수 카운팅
lsns --no-headings | awk '{print $2}' | sort | uniq -c | sort -rn
# 143 cgroup
# 143 pid
# 143 user
# 143 uts
# 50 mnt ← 컨테이너가 50개
# 50 net
# sysctl로 namespace 생성 제한 확인/설정
sysctl user.max_user_namespaces
sysctl user.max_pid_namespaces
sysctl user.max_net_namespaces
sysctl user.max_mnt_namespaces
# /proc/sys/user/ 아래의 모든 namespace 제한 확인
cat /proc/sys/user/max_*_namespaces
ftrace로 네임스페이스 이벤트 추적
커널의 ftrace 인프라를 사용하면 네임스페이스 생성/삭제/전환을 실시간으로 추적할 수 있습니다. 특히 copy_namespaces(), switch_task_namespaces(), setns() 같은 함수를 추적하면 컨테이너 런타임의 동작을 이해하는 데 도움이 됩니다.
# ftrace로 네임스페이스 관련 커널 함수 추적
# 1. 추적 가능한 네임스페이스 관련 함수 확인
cat /sys/kernel/debug/tracing/available_filter_functions | grep -E '(ns_|namespace|nsproxy|setns|unshare)'
# 2. function graph tracer로 setns 흐름 추적
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo '__x64_sys_setns' > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 3. 다른 터미널에서 setns 호출 유발
nsenter --target $PID --net ip addr show
# 4. 추적 결과 확인
cat /sys/kernel/debug/tracing/trace
# __x64_sys_setns() {
# prepare_nsset() {
# create_new_namespaces() {
# copy_net_ns() { ... }
# }
# }
# validate_nsset() { ... }
# commit_nsset() {
# switch_task_namespaces() { ... }
# }
# }
# 5. 추적 종료
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo nop > /sys/kernel/debug/tracing/current_tracer
# === bpftrace로 네임스페이스 이벤트 관찰 ===
# (eBPF 기반, 더 유연한 추적)
# clone/unshare로 네임스페이스 생성 추적
bpftrace -e '
kprobe:create_new_namespaces {
printf("pid=%d comm=%s creating new namespaces\n",
pid, comm);
}
'
# setns 호출 추적 (어떤 프로세스가 어디에 진입하는지)
bpftrace -e '
tracepoint:syscalls:sys_enter_setns {
printf("pid=%d comm=%s setns(fd=%d, flags=0x%x)\n",
pid, comm, args->fd, args->flags);
}
'
# 네임스페이스 해제 추적 (수명 관리 디버깅)
bpftrace -e '
kprobe:put_net {
printf("pid=%d releasing net namespace\n", pid);
}
'
자주 발생하는 운영 문제와 해결법
| 증상 | 원인 | 진단 방법 | 해결 방법 |
|---|---|---|---|
| 컨테이너에서 DNS 조회 실패 | Net NS에 기본 라우트(default route)가 없거나, /etc/resolv.conf가 잘못됨 |
nsenter --net ip route, nsenter --mount cat /etc/resolv.conf |
기본 라우트 추가, DNS 설정 확인 |
| 컨테이너 간 통신 불가 | iptables FORWARD 체인 DROP, 브릿지 설정 오류 | iptables -L FORWARD, bridge fdb show |
net.bridge.bridge-nf-call-iptables 확인 |
PID NS 내에서 ps가 빈 결과 |
/proc가 PID NS에 맞게 재마운트되지 않음 |
mount | grep proc |
mount -t proc proc /proc |
unshare가 EPERM으로 실패 |
user.max_*_namespaces가 0이거나, seccomp 차단 |
sysctl user.max_user_namespaces, dmesg |
sysctl 제한값 조정 |
setns가 EINVAL로 실패 |
flags와 fd의 namespace 유형 불일치 | readlink /proc/PID/ns/TYPE으로 유형 확인 |
올바른 CLONE_NEW* 플래그 사용 |
| 네임스페이스 수가 계속 증가 | CNI 플러그인이 bind mount를 해제하지 않음 | lsns | wc -l, grep nsfs /proc/mounts |
bind mount 해제, CNI 플러그인 업그레이드 |
컨테이너 시작 시 ENOMEM |
마운트 포인트가 너무 많아 Mount NS 복사 시 메모리 부족 | findmnt | wc -l, cat /proc/sys/fs/mount-max |
불필요한 마운트 제거, mount-max 조정 |
| 좀비 프로세스 누적 (컨테이너 내) | PID 1 프로세스가 wait()를 호출하지 않음 |
nsenter --pid ps aux | grep Z |
tini/dumb-init 사용, --init 플래그 |
# === 자주 사용하는 디버깅 원라이너 ===
# 특정 컨테이너의 열린 포트 확인 (호스트에서)
nsenter --target $PID --net ss -tlnp
# 컨테이너의 네트워크 트래픽 캡처
nsenter --target $PID --net tcpdump -i any -c 100 -w /tmp/container.pcap
# 컨테이너의 라우팅 테이블과 ARP 테이블 확인
nsenter --target $PID --net ip route show
nsenter --target $PID --net ip neigh show
# 컨테이너의 iptables/nftables 규칙 확인
nsenter --target $PID --net nft list ruleset 2>/dev/null || \
nsenter --target $PID --net iptables-save
# 모든 네트워크 네임스페이스의 인터페이스 목록
for ns in $(ip netns list | awk '{print $1}'); do
echo "=== $ns ==="
ip netns exec $ns ip -br addr show
done
# Docker 컨테이너의 veth 페어 매핑 찾기
CONTAINER_IFINDEX=$(nsenter --target $PID --net cat /sys/class/net/eth0/iflink)
ip link show | grep "^$CONTAINER_IFINDEX:"
# 호스트에서 해당 veth 인터페이스 확인
# 마운트 전파(propagation) 문제 진단
findmnt -o TARGET,PROPAGATION | grep shared
# shared 마운트가 너무 많으면 성능 영향
# 네임스페이스별 프로세스 수 확인
lsns -t pid -o NS,NPROCS --no-headings | sort -k2 -rn | head -10
# 프로세스가 많은 PID NS 상위 10개
Kubernetes와 네임스페이스
Kubernetes(쿠버네티스)에서 Pod는 네임스페이스의 공유 단위입니다. 같은 Pod 내의 컨테이너들은 일부 네임스페이스를 공유하고, 일부는 격리합니다. 이 공유 모델을 이해하는 것이 Pod 네트워킹과 보안 설계의 핵심입니다.
| 네임스페이스 | Pod 내 컨테이너 간 | Pod 간 | 의미 |
|---|---|---|---|
| Network | 공유 | 격리 | 같은 Pod 컨테이너는 localhost로 통신, 같은 IP 사용 |
| IPC | 공유 | 격리 | 같은 Pod 컨테이너는 SysV IPC/POSIX MQ 공유 |
| UTS | 공유 | 격리 | 같은 Pod 컨테이너는 같은 호스트명 사용 |
| PID | 선택적 공유 | 격리 | shareProcessNamespace: true 시 공유 |
| Mount | 격리 | 격리 | 각 컨테이너 고유 파일시스템 (emptyDir 등으로 공유 가능) |
| User | 보통 공유 | 격리 | User NS는 쿠버네티스에서 아직 제한적 지원 |
| Cgroup | 공유 | 격리 | Pod 단위 cgroup |
# Kubernetes Pod에서 PID 네임스페이스 공유 설정
apiVersion: v1
kind: Pod
metadata:
name: shared-pid-example
spec:
shareProcessNamespace: true # PID NS 공유 활성화
containers:
- name: app
image: nginx
- name: sidecar
image: busybox
command: ["sh", "-c", "while true; do ps aux; sleep 10; done"]
# sidecar에서 nginx 프로세스를 볼 수 있음
쿠버네티스의 각 Pod에는 보이지 않는 pause 컨테이너(인프라 컨테이너)가 먼저 생성됩니다. 이 컨테이너가 Network/IPC/UTS 네임스페이스를 생성하고, Pod 내의 다른 컨테이너들은 setns()를 통해 pause 컨테이너의 네임스페이스에 진입합니다. pause 컨테이너는 pause() 시스템 콜만 호출하며, Pod의 네임스페이스 앵커(anchor) 역할을 합니다. 앱 컨테이너가 재시작되어도 네트워크 설정이 유지되는 이유입니다.
네임스페이스를 활용한 최소 컨테이너 구현
아래 예제는 네임스페이스 API만으로 최소한의 컨테이너를 구현하는 C 프로그램입니다. 실제 컨테이너 런타임(runc)이 수행하는 핵심 단계를 압축하여 보여줍니다.
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mount.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#define STACK_SIZE (1024 * 1024)
struct container_config {
char *hostname;
char *rootfs; /* 예: "./alpine-rootfs" */
char **argv; /* 실행할 명령 */
};
static int container_init(void *arg)
{
struct container_config *cfg = (struct container_config *)arg;
/* 1단계: 호스트명 설정 (UTS NS 격리) */
sethostname(cfg->hostname, strlen(cfg->hostname));
/* 2단계: Mount propagation을 private으로 설정 */
mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL);
/* 3단계: 새 루트 파일시스템으로 pivot_root */
char oldroot[256];
snprintf(oldroot, sizeof(oldroot), "%s/.oldroot", cfg->rootfs);
mkdir(oldroot, 0700);
mount(cfg->rootfs, cfg->rootfs, NULL, MS_BIND | MS_REC, NULL);
syscall(SYS_pivot_root, cfg->rootfs, oldroot);
chdir("/");
/* 4단계: 이전 루트 언마운트 */
umount2("/.oldroot", MNT_DETACH);
rmdir("/.oldroot");
/* 5단계: /proc 마운트 (PID NS에 맞는 프로세스 목록) */
mkdir("/proc", 0555);
mount("proc", "/proc", "proc", 0, NULL);
/* 6단계: /dev/null 등 최소 디바이스 */
mkdir("/dev", 0755);
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755");
/* 7단계: 사용자 명령 실행 */
execvp(cfg->argv[0], cfg->argv);
perror("exec");
return 1;
}
int main(int argc, char **argv)
{
if (argc < 3) {
fprintf(stderr, "Usage: %s <rootfs> <cmd> [args...]\n", argv[0]);
return 1;
}
struct container_config cfg = {
.hostname = "mini-container",
.rootfs = argv[1],
.argv = &argv[2],
};
char *stack = malloc(STACK_SIZE);
int flags = CLONE_NEWPID /* 새 PID NS */
| CLONE_NEWNS /* 새 Mount NS */
| CLONE_NEWUTS /* 새 UTS NS */
| CLONE_NEWIPC /* 새 IPC NS */
| CLONE_NEWNET /* 새 Network NS */
| CLONE_NEWCGROUP /* 새 Cgroup NS */
| SIGCHLD;
pid_t child = clone(container_init, stack + STACK_SIZE, flags, &cfg);
if (child == -1) {
perror("clone");
return 1;
}
printf("Container PID (host view): %d\n", child);
int status;
waitpid(child, &status, 0);
free(stack);
return WEXITSTATUS(status);
}
/* 빌드 및 실행:
* gcc -o mini-container mini-container.c
* # Alpine rootfs 준비 (docker export 또는 debootstrap)
* mkdir -p ./alpine-rootfs
* docker export $(docker create alpine) | tar -C ./alpine-rootfs -xf -
* sudo ./mini-container ./alpine-rootfs /bin/sh
*/
CRIU와 네임스페이스 체크포인트/복원
CRIU(Checkpoint/Restore In Userspace)는 실행 중인 프로세스의 상태를 파일로 저장(체크포인트)하고 나중에 복원하는 도구입니다. 컨테이너 라이브 마이그레이션(Live Migration)의 핵심 기술이며, 네임스페이스 상태를 정확히 보존하고 복원해야 합니다.
| 네임스페이스 | 체크포인트 저장 내용 | 복원 시 처리 | 주요 과제 |
|---|---|---|---|
| PID | PID 번호, 계층 구조 | clone3(set_tid)로 원래 PID 복원 | PID 충돌 회피 |
| Network | 인터페이스, 라우팅, iptables, 열린 소켓 | netlink로 네트워크 재구성 | TCP 연결 상태 복원 |
| Mount | 마운트 트리, propagation 유형 | 순서대로 마운트 재구성 | 외부 스토리지 의존성 |
| Time | monotonic/boottime 오프셋 | CLONE_NEWTIME + 오프셋 설정 | Time NS 도입의 주 동기 |
| User | UID/GID 매핑 | 매핑 테이블 복원 | 한 번만 쓸 수 있는 제약 |
| IPC | SysV IPC 객체 상태 | IPC 객체 재생성 | 키/ID 일치 보장 |
# CRIU로 컨테이너 체크포인트/복원 (Podman 사용)
# 1. 컨테이너 실행
podman run -d --name myapp redis
# 2. 체크포인트 생성 (모든 네임스페이스 상태 저장)
podman container checkpoint myapp --export=/tmp/myapp-checkpoint.tar.gz
# 3. 다른 노드에서 복원
podman container restore --import=/tmp/myapp-checkpoint.tar.gz --name=myapp-restored
# 4. 복원된 컨테이너의 네임스페이스 확인
RESTORED_PID=$(podman inspect myapp-restored --format '{{.State.Pid}}')
ls -la /proc/$RESTORED_PID/ns/
# 새로운 네임스페이스 inode이지만, 내부 상태는 원본과 동일
네임스페이스 명령어 치트 시트
| 작업 | 명령어 | 필요 권한 |
|---|---|---|
| 모든 NS 조회 | lsns | root (전체 목록), 일반 (자기 것만) |
| 특정 유형 NS 조회 | lsns -t net | root |
| 프로세스의 NS 확인 | ls -la /proc/PID/ns/ | 일반 |
| 두 프로세스 NS 비교 | readlink /proc/PID1/ns/net vs /proc/PID2/ns/net | 일반 |
| PID NS 계층 확인 | grep NSpid /proc/PID/status | 일반 |
| 새 UTS NS 생성 | unshare --uts /bin/bash | root 또는 User NS |
| 새 PID+Mount NS 생성 | unshare --pid --fork --mount-proc /bin/bash | root |
| 새 Net NS 생성 | ip netns add myns | root |
| Net NS 진입 | ip netns exec myns command | root |
| 모든 NS 진입 | nsenter --target PID --all /bin/bash | root |
| 특정 NS만 진입 | nsenter --target PID --net command | root |
| NS bind mount | mount --bind /proc/PID/ns/net /run/ns/mynet | root |
| NS bind mount 해제 | umount /run/ns/mynet | root |
| User NS 생성 (비특권) | unshare --user --map-root-user /bin/bash | 일반 사용자 |
| UID 매핑 확인 | cat /proc/PID/uid_map | 일반 |
| NS 개수 제한 확인 | sysctl user.max_user_namespaces | 일반 |
| NS 개수 제한 설정 | sysctl -w user.max_user_namespaces=0 | root |
| Docker 컨테이너 PID | docker inspect --format '{{.State.Pid}}' NAME | docker 그룹 |
| Docker 컨테이너 NS 진입 | nsenter --target $(docker inspect -f '{{.State.Pid}}' NAME) --net command | root |
| veth 페어 생성 | ip link add veth0 type veth peer name veth1 | root |
| 인터페이스 NS 이동 | ip link set veth1 netns myns | root |
커널 설정 옵션
네임스페이스 기능을 사용하려면 커널 빌드 시 해당 설정이 활성화되어 있어야 합니다. 대부분의 배포판 커널은 이미 활성화되어 있습니다.
| 커널 설정 | 네임스페이스 | 설명 |
|---|---|---|
CONFIG_NAMESPACES | 전체 | 네임스페이스 인프라 (필수) |
CONFIG_UTS_NS | UTS | 호스트명 격리 |
CONFIG_IPC_NS | IPC | IPC 객체 격리 |
CONFIG_PID_NS | PID | PID 격리 |
CONFIG_NET_NS | Network | 네트워크 스택 격리 |
CONFIG_USER_NS | User | UID/GID 매핑 |
CONFIG_CGROUP_NS | Cgroup | cgroup 뷰 격리 |
CONFIG_TIME_NS | Time | 시간 오프셋 격리 |
# 현재 커널의 네임스페이스 설정 확인
zcat /proc/config.gz 2>/dev/null | grep -E '(NAMESPACES|_NS=)' || \
grep -E '(NAMESPACES|_NS=)' /boot/config-$(uname -r)
# 결과 예시:
# CONFIG_NAMESPACES=y
# CONFIG_UTS_NS=y
# CONFIG_IPC_NS=y
# CONFIG_USER_NS=y
# CONFIG_PID_NS=y
# CONFIG_NET_NS=y
# CONFIG_CGROUP_NS=y (CONFIG_CGROUPS 필요)
# CONFIG_TIME_NS=y
참고 링크
- namespaces(7) — 리눅스 네임스페이스 전체 개요 매뉴얼 페이지입니다
- user_namespaces(7) — User 네임스페이스의 UID/GID 매핑과 권한 규칙을 설명합니다
- pid_namespaces(7) — PID 네임스페이스의 계층 구조와 init 프로세스 동작을 다룹니다
- mount_namespaces(7) — Mount 네임스페이스와 공유/비공유 전파 모드를 설명합니다
- network_namespaces(7) — 네트워크 네임스페이스의 인터페이스 격리와 veth 페어를 다룹니다
- uts_namespaces(7) — UTS 네임스페이스의 호스트명·도메인명 격리를 설명합니다
- ipc_namespaces(7) — IPC 네임스페이스의 System V IPC 및 POSIX 메시지 큐 격리를 다룹니다
- cgroup_namespaces(7) — cgroup 네임스페이스의 가상화된 cgroup 계층 뷰를 설명합니다
- unshare(2) — 현재 프로세스를 새 네임스페이스로 분리하는 시스템 콜입니다
- setns(2) — 기존 네임스페이스에 합류하는 시스템 콜입니다
- clone(2) — 새 네임스페이스와 함께 자식 프로세스를 생성하는 시스템 콜입니다
- LWN: Namespaces in operation (series) — Michael Kerrisk의 네임스페이스 연재 시리즈 첫 번째 글입니다
- LWN: Namespaces: user namespaces — User 네임스페이스의 설계와 보안 고려 사항을 심층 분석합니다
- LWN: Namespaces: mount namespaces — Mount 네임스페이스의 전파 타입과 활용 패턴을 다룹니다
- LWN: The completion of the user-namespace journey — User 네임스페이스 개발 과정의 완결과 최종 설계를 정리합니다
kernel/nsproxy.c— 네임스페이스 프록시 구조, copy_namespaces(), switch_task_namespaces() 구현부입니다kernel/pid_namespace.c— PID 네임스페이스 생성과 해제 로직을 포함합니다kernel/user_namespace.c— User 네임스페이스 및 UID/GID 매핑 처리를 담당합니다fs/namespace.c— Mount 네임스페이스의 핵심 구현을 포함합니다fs/proc/namespaces.c— /proc/[pid]/ns/ 인터페이스를 제공합니다include/linux/nsproxy.h— nsproxy 구조체 정의를 포함합니다- OCI Runtime Specification — Namespaces — 컨테이너 런타임의 네임스페이스 설정 표준 명세입니다
- Kernel Documentation — Namespaces compatibility list — 커널 공식 문서의 네임스페이스 호환성 목록입니다
관련 문서
네임스페이스와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.