네임스페이스 (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), 운영 중 장애 원인 추적까지 실제 컨테이너 인프라에 필요한 핵심 내용을 다룹니다.

전제 조건: 프로세스 관리cgroups 문서를 먼저 읽으세요. 컨테이너 격리는 커널 객체 자체를 복제하는 것이 아니라 가시성 범위를 분리하는 방식이므로, 프로세스 구조와 리소스 제어를 먼저 이해해야 합니다.
일상 비유: 이 주제는 공유 건물의 층별 출입 통제와 비슷합니다. 건물은 같지만 각 층의 출입 권한과 표지판이 다르듯이, 네임스페이스는 같은 커널 위에서 관측 범위를 분리합니다.

핵심 요약

  • nsproxy — task가 참조하는 네임스페이스 집합
  • clone/unshare/setns — 생성/분리/진입 API
  • User NS — 권한 매핑과 공격면에 가장 큰 영향
  • Mount/Net/PID NS — 컨테이너 격리의 실질 핵심
  • pidfd — PID 재사용 문제를 줄이는 현대적 제어 방식

단계별 이해

  1. 개념 분해
    어떤 자원이 어떤 네임스페이스에 의해 격리되는지 표로 정리합니다.
  2. API 실습
    unshare --mount --pid --fork 같은 최소 예제로 동작을 확인합니다.
  3. 런타임 연결
    containerd/runc가 생성 단계에서 어떤 API를 호출하는지 추적합니다.
  4. 보안 점검
    User NS 허용 범위와 관련 sysctl 정책을 운영 환경에 맞춥니다.
관련 표준: OCI Runtime Specification 1.0 (네임스페이스 격리 요구사항), POSIX.1-2017 (프로세스 격리 기반) — 컨테이너 런타임의 네임스페이스 활용 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

네임스페이스 개요

네임스페이스(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 / macvlanvirtio-net / SR-IOV
하이브리드 접근: Kata Containers, gVisor, Firecracker 같은 기술은 컨테이너의 편의성과 가상 머신의 보안 강도를 결합합니다. Kata Containers는 각 컨테이너(또는 Pod)를 경량 VM 안에서 실행하고, gVisor는 사용자 공간(User Space) 커널을 통해 syscall을 중재합니다.

네임스페이스 도입 타임라인

Linux 네임스페이스 도입 타임라인 Mount CLONE_NEWNS 2.4.19 (2002) UTS / IPC NEWUTS / NEWIPC 2.6.19 (2006) PID CLONE_NEWPID 2.6.24 (2008) Network CLONE_NEWNET 2.6.29 (2009) User CLONE_NEWUSER 3.8 (2013) Cgroup CLONE_NEWCGROUP 4.6 (2016) Time CLONE_NEWTIME 5.6 (2020) 커널 버전별 주요 네임스페이스 관련 기능 추가 3.8: User NS가 non-root에게 namespace 생성 허용 (컨테이너 보안 모델의 전환점) 5.2: clone3() syscall 도입 — 확장 가능한 namespace 생성 API 5.3: pidfd_open() — PID 재사용 경쟁 없는 프로세스 제어 5.8: setns(pidfd, flags) — 여러 namespace를 원자적으로 진입

네임스페이스 격리 원리

네임스페이스의 핵심 원리는 "같은 커널 위에서 서로 다른 시스템처럼 보이게 만든다"는 것입니다. 운영체제 수준의 가상화(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)해서는 충분하지 않습니다.

task_struct → nsproxy → namespaces 참조 구조 프로세스 (task_struct) PID 100 (init) nsproxy → 0x12345 cred → user_ns PID 200 (nginx) nsproxy → 0x12345 공유 (same ptr) PID 1234 (container) nsproxy → 0xABCDE 새 namespace 집합 cred → user_ns (UID 매핑) nsproxy (0x12345) Host Namespaces uts_ns → host_uts ipc_ns → host_ipc mnt_ns → host_mnt net_ns → host_net pid_ns_for_children nsproxy (0xABCDE) Container Namespaces uts_ns → container_uts mnt_ns → container_mnt net_ns → container_net ... 실제 Namespace 객체 uts_namespace hostname, domainname ipc_namespace SysV IPC, POSIX MQ mnt_namespace mount tree net namespace network stack pid_namespace PID 번역 time_namespace clock offsets cgroup_namespace cgroup view user_namespace cred에서 참조 참조 카운팅: 같은 nsproxy를 공유하는 프로세스는 동일 namespace 집합 사용 | 마지막 참조 해제 시 자동 정리

네임스페이스 격리의 동작 방식은 다음과 같습니다:

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가 추가로 개입합니다.

task_struct가 namespace를 해석하는 실제 경로 current task_struct nsproxy 대부분의 namespace 묶음 cred UID/GID, capability, user_ns thread_pid / struct pid 현재 활성 PID 뷰의 근거 fs_struct root / pwd mount namespace 위에서 경로 해석 struct nsproxy task가 공유할 수 있는 namespace 집합 uts_ns / ipc_ns / mnt_ns / net_ns time_ns / cgroup_ns pid_ns_for_children 다음 fork/clone 자식이 들어갈 PID NS time_ns_for_children 다음 자식의 time namespace 후보 현재 가시 자원 UTS, IPC, Mount, Net, Cgroup 대부분은 task -> nsproxy에서 바로 출발 현재 Time view current->nsproxy->time_ns 의 오프셋 적용 현재 PID view task_active_pid_ns(task) struct pid 계층에서 계산 권한 view cred->user_ns capability / UID map 기준 다음 자식의 view pid_ns_for_children / time_ns_for_children unshare(CLONE_NEWPID/CLONE_NEWTIME) 후 먼저 바뀌는 영역 핵심: nsproxy만 보면 절반은 맞지만, PID/User/경로 해석은 추가 맥락이 필요
대부분의 namespace는 nsproxy가 출발점이지만, 현재 PID 뷰는 task_active_pid_ns(task), 권한은 cred->user_ns, 경로 해석은 fs_struct가 함께 관여합니다.
조회 대상실제 출발점현재 task에서 의미핵심 주의점
UTS / IPC / Mount / Net / Cgrouptask->nsproxy현재 task가 바로 보는 자원 범위대부분의 namespace는 이 경로에서 결정됩니다.
현재 Time viewtask->nsproxy->time_ns현재 task에 적용되는 monotonic / boottime 오프셋(Offset)읽기 경로는 현재 task의 time_ns를 즉시 참조합니다.
현재 PID viewtask_active_pid_ns(task)현재 task가 속한 활성 PID namespacepid_ns_for_children와 동일하다고 가정하면 잘못된 진단이 나옵니다.
자식 PID viewtask->nsproxy->pid_ns_for_children다음 fork / clone 자식이 들어갈 PID namespaceunshare(CLONE_NEWPID) 직후 현재 task의 PID는 바뀌지 않습니다.
자식 Time viewtask->nsproxy->time_ns_for_children다음 자식이 상속할 time namespaceTime NS는 exec_task_namespaces()까지 엮이는 특수 경로가 있습니다.
User NS / capabilitytask->cred->user_nsUID/GID 매핑과 권한 판정 기준User NS는 nsproxy가 아니라 cred에 붙습니다.
root / pwdtask->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_nsstruct ns_common ns가 들어 있습니다. 즉 nsproxy는 "어떤 namespace 객체를 볼 것인가"를 결정하고, cred->user_ns는 "그 객체를 조작할 권한이 있는가"를 판정합니다.

예외: PID와 Time은 현재 뷰와 자식 뷰가 분리됩니다

nsproxy를 이해할 때 가장 자주 놓치는 부분이 바로 pid_ns_for_childrentime_ns_for_children입니다. 이름 그대로 이 둘은 현재 task 자체가 즉시 쓰는 namespace가 아니라, 앞으로 만들어질 자식에게 적용할 namespace입니다. 이 설계는 "이미 존재하는 task의 PID를 중간에 갈아끼울 수 없는" 현실과, Time NS가 vvar/vDSO와 맞물려 단계적으로 전환되어야 하는 제약에서 나옵니다.

# 현재 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_nsmnt_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, 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)
nsproxy가 생성되고 교체되는 세 경로 1. clone / clone3 copy_process() CLONE_NEW* 포함? 아니오 get_nsproxy(old) child가 공유 create_new_namespaces() 새 nsproxy 부착 공유 경로는 빠르고, 생성 경로는 새 묶음을 준비 2. unshare ksys_unshare() unshare_userns(), unshare_fs() 멀티스레드 / CLONE_FS 제약 검사 unshare_nsproxy_namespaces() create_new_namespaces()로 새 묶음 준비 switch_task_namespaces(current, new) 현재 task 문맥을 교체 3. setns setns(fd 또는 pidfd, flags) prepare_nsset() validate_ns() / validate_nsset() 대상 task 스냅샷, 권한, 종류 검증 commit_nsset() cred / fs / IPC / time 정리 후 switch 특수점: CLONE_NEWPID 와 CLONE_NEWTIME 는 현재 task보다 "다음 자식"에 먼저 반영되는 경우가 있어 fork / exec 경계를 함께 봐야 합니다.
clone()은 보통 공유 경로가 빠르고, unshare()setns()는 새 nsproxy 묶음을 준비한 뒤 최종 commit 단계에서 현재 task에 전환합니다.

clone 경로: 최신 kernel/nsproxy.ccopy_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_NEWUSERCLONE_THREAD | CLONE_FS를 자동 포함시키고, CLONE_NEWNSCLONE_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의 nsproxycred를 스냅샷으로 잡은 뒤, 모든 검증이 끝난 후 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 객체를 가리킵니다.

namespace 객체가 nsfs 파일 디스크립터로 노출되는 과정 concrete namespace net / mnt / pid / time ... 실제 커널 자원과 상태 struct ns_common ops, inum, count proc_ns_operations 와 연결 readlink 시 name:[inum] 표기 /proc/1234/ns/net procfs 엔트리 open(), readlink(), stat() 가능 setns() 입력 핸들로 사용 open fd / bind mount 열린 파일 디스크립터 또는 /run/ns/demo 로 bind namespace 수명을 고정 의미 마지막 task가 종료해도 열린 fd나 bind mount가 남아 있으면 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로 재진입할 수 있습니다.

실전 디버깅 체크리스트

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프로세스 IDCLONE_NEWPID
Mount마운트 포인트CLONE_NEWNS
Network네트워크 스택(Network Stack)CLONE_NEWNET
UserUID/GID 매핑CLONE_NEWUSER
UTS호스트명, 도메인명CLONE_NEWUTS
IPCSystem V IPC, POSIX MQCLONE_NEWIPC
CgroupCgroup 루트 디렉터리CLONE_NEWCGROUP
TimeCLOCK_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은 같음
"

네임스페이스 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_NEWPIDPID자식이 새 NS의 PID 1다음 fork() 자식에만 적용다음 fork() 자식에만 적용현재 PID는 변하지 않음
CLONE_NEWNS마운트마운트 트리 복사마운트 트리 복사즉시 적용생성 비용 높음 (마운트 수 비례)
CLONE_NEWNET네트워크lo만 있는 빈 스택lo만 있는 빈 스택즉시 적용열린 소켓은 원래 NS에 유지
CLONE_NEWUSERUID/GID즉시 새 User NS즉시 새 User NS즉시 적용비특권 사용자도 생성 가능
CLONE_NEWUTS호스트명복사 후 격리복사 후 격리즉시 적용매우 가벼움
CLONE_NEWIPCIPC빈 IPC 테이블빈 IPC 테이블즉시 적용기존 IPC 객체는 복사 안 함
CLONE_NEWCGROUPcgroup 뷰현재 위치가 루트현재 위치가 루트즉시 적용가시성만 격리
CLONE_NEWTIME시간 오프셋자식에 적용다음 exec()에 적용다음 exec()에 적용vDSO 재매핑 필요

PID Namespace

PID 네임스페이스는 프로세스 ID를 격리합니다. 새 PID NS 내의 첫 프로세스는 PID 1이 되며, 부모 NS에서는 다른 PID로 보입니다. PID 네임스페이스는 계층적(hierarchical) 구조를 형성하며, 부모 네임스페이스에서는 자식 네임스페이스의 모든 프로세스를 볼 수 있지만, 자식에서는 부모의 프로세스를 볼 수 없습니다.

PID 1 (init 프로세스)의 특수성

각 PID 네임스페이스의 PID 1 프로세스는 그 네임스페이스의 init 프로세스 역할을 합니다. 이 프로세스는 두 가지 중요한 특성을 가집니다:

/* 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
'
PID Namespace 계층 구조와 PID 번역 Host PID Namespace (level 0) systemd PID 1 sshd PID 500 container-shim PID 1000 nginx (container) PID 1234 worker PID 1235 Container PID Namespace (level 1) nginx (init) PID 1 (= Host PID 1234) worker PID 2 (= Host PID 1235) /proc mount 이 NS의 프로세스만 표시 struct pid 번역 (nginx 프로세스 예시) numbers[0] = { nr = 1234, ns = host_pid_ns } numbers[1] = { nr = 1, ns = container_pid_ns } level = 1 (중첩 깊이) task_pid_nr_ns(task, container_ns) → 1 task_pid_nr_ns(task, host_ns) → 1234
하나의 프로세스가 struct pidnumbers[] 배열을 통해 각 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 → 외부
veth 페어를 통한 네트워크 네임스페이스 격리 Host Network Namespace veth0 10.0.0.1/24 UP, RUNNING Host 네트워크 스택 (라우팅 테이블, iptables, 소켓 등) Container Network Namespace veth1 10.0.0.2/24 UP, RUNNING Container 네트워크 스택 (독립된 라우팅 테이블, iptables) 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
💡

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도 불가
Mount Propagation 유형 비교 shared Host NS Child NS 양방향 전파 peer group 공유 mount/umount 이벤트 동기화 slave Host NS Child NS 단방향 전파 Host → Child만 Child 마운트는 격리됨 private Host NS Child NS 전파 없음 완전 격리 독립적인 마운트 트리 unbindable Host NS Child NS bind mount 차단 private + 추가 제한 보안 격리에 유용 컨테이너 환경에서의 권장 사용 shared 활용 • 호스트↔컨테이너 동기화 • 동적 볼륨 마운트 • Kubernetes CSI 기본값 (unshare 시) propagation 미지정 시 slave 활용 • 호스트 마운트 수신만 • 컨테이너 격리 강화 • systemd-nspawn 보안/격리 균형 Docker 기본 설정 private 활용 • 완전 독립 환경 • 임시 컨테이너 • 테스트 격리 최대 격리 --propagation private unbindable 활용 • 보안 크리티컬 경로 • /proc, /sys 보호 • 재마운트 방지 보안 강화 샌드박스 환경

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;
};
User NS 계층 구조와 namespace 소유 관계 init_user_ns (level 0) UID/GID 매핑 없음 (1:1) | 호스트 root = 진짜 root user_ns_A (level 1) UID 0 → 호스트 UID 1000 | 컨테이너 A user_ns_B (level 1) UID 0 → 호스트 UID 2000 | 컨테이너 B 호스트 net_ns, mnt_ns, pid_ns ... owner: init_user_ns 컨테이너 A의 net/mnt/pid NS owner: user_ns_A 컨테이너 B의 net/mnt/pid NS owner: user_ns_B ns_capable(net->user_ns, CAP_NET_ADMIN): current의 user_ns가 net NS 소유자 계층에 있어야 성공
모든 non-user namespace는 생성 시점의 User 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_nsinit_user_ns인정같은 user_ns
컨테이너 root가 자기 net_ns 조작user_ns_Auser_ns_A인정같은 user_ns
컨테이너 root가 호스트 net_ns 조작user_ns_Ainit_user_ns거부user_ns_A는 init_user_ns의 자식
호스트 root가 컨테이너 net_ns 조작init_user_nsuser_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
setgroups deny 규칙:

비특권 사용자가 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 SEMMNI32000 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())가장 일반적인 참조 소스
열린 fdopen("/proc/PID/ns/...")close(fd)fd 누수 시 네임스페이스 잔류
bind mountmount --bind /proc/PID/ns/net /run/...umount()CNI 플러그인이 자주 사용
자식 네임스페이스User NS 계층 구조자식 NS 소멸User NS는 자식이 있으면 유지됨
소유된 non-user NSNS 생성 시 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.1pidfd_send_signal()PID NS 경계에서 안전한 시그널
clone3 + CLONE_PIDFD5.2clone3()NS 생성 + pidfd 동시 획득
pidfd_open5.3pidfd_open()기존 프로세스의 pidfd 획득
pidfd_getfd5.6pidfd_getfd()NS 경계 넘어 fd 복제
setns + pidfd5.8setns(pidfd, flags)한 번에 여러 NS 원자 진입
waitid + P_PIDFD5.4waitid()pidfd로 자식 대기

실전 디버깅: nsenter와 lsns

운영 환경에서 네임스페이스 문제를 진단할 때 가장 많이 사용하는 도구는 nsenterlsns입니다. 각 도구의 정확한 사용법과 자주 발생하는 함정을 정리합니다.

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 실패 원인 체크리스트:

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를 생성합니다.

컨테이너 런타임의 namespace 생성 흐름 (runc 기준) containerd OCI spec JSON 준비 runc create config.json 파싱 runc init (부모) clone3(CLONE_NEW*) 호출 runc init (자식) 새 NS 안에서 exec runc이 생성하는 namespace 조합 (일반적인 컨테이너) PID NS PID 1 = entrypoint Mount NS pivot_root + overlay Net NS veth + bridge UTS NS hostname 격리 IPC NS SysV IPC 격리 Cgroup NS cgroup 뷰 격리 User NS (선택적) rootless 모드 시 활성화 | UID 매핑 필요 Time NS (선택적) CRIU 체크포인트/복원 시 활성화 추가 보안 계층 seccomp + AppArmor/SELinux + capabilities drop 생성 순서: User NS (있으면) → PID → Mount → Net → UTS → IPC → Cgroup → Time User NS가 먼저여야 나머지 NS 생성 시 capability가 인정됩니다. 이후 pivot_root → seccomp → exec(entrypoint)
컨테이너 런타임(runc)은 OCI 스펙에 따라 namespace를 조합합니다. User NS → 나머지 NS → mount 설정(pivot_root) → seccomp/capabilities 적용 → exec 순서입니다.

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 NSdocker0 bridge + vethslirp4netns 또는 pasta
PID NS inittini (docker-init)conmon (container monitor)
보안 강화seccomp + AppArmorseccomp + SELinux
cgroup 드라이버cgroupfs 또는 systemdsystemd 기본

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.19CLONE_NEWNS
UTS호스트네임, 도메인네임2.6.19CLONE_NEWUTS
IPCSystem V IPC, POSIX MQ2.6.19CLONE_NEWIPC
PID프로세스 ID2.6.24CLONE_NEWPID
Network네트워크 스택2.6.29CLONE_NEWNET
UserUID/GID 매핑3.8CLONE_NEWUSER
Cgroupcgroup 루트 디렉토리4.6CLONE_NEWCGROUP
Time부트 시간, 모노토닉 시계5.6CLONE_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) 모델

컨테이너 보안 다층 방어 모델 계층 1: Namespaces (가시성 격리) PID / Mount / Network / UTS / IPC / Cgroup / Time + User NS (권한 매핑) 계층 2: cgroups (리소스 제한) CPU / Memory / I/O / PID 수 / 네트워크 대역폭 제한 계층 3: Capabilities (세분화된 권한) 필요한 capability만 유지 (drop ALL + add 필요한 것) 계층 4: seccomp (시스템 콜 필터) 허용된 syscall만 통과, 위험한 syscall (mount, ptrace, ...) 차단 계층 5: LSM (AppArmor / SELinux) 파일 접근, 네트워크 접근, capability 사용을 정책 기반으로 제어
컨테이너 보안은 단일 메커니즘이 아니라 여러 계층의 조합으로 달성됩니다. 네임스페이스는 가시성 격리만 제공하며, 나머지 계층과 함께 사용해야 효과적입니다.

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_clone1 (Debian/Ubuntu)0비특권 User NS 생성 차단
kernel.unprivileged_bpf_disabled22User NS 내 BPF 프로그램 로드 차단
kernel.perf_event_paranoid23User 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)
# 하지만 호스트 자원에 대해서는 무력
namespace가 보안 경계가 아닌 이유:

(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 자체의 보안 설계와 밀접하게 연관됩니다.

CVE-2022-0185 — fsconfig 힙 오버플로우 (네임스페이스 탈출):

fsconfig() 시스템 콜의 legacy_parse_param()에서 파라미터 길이 검증 부족으로 정수 언더플로우가 발생합니다. User namespace 내의 CAP_SYS_ADMIN만으로 트리거 가능하며, 힙 오버플로우를 통해 init namespace로 탈출할 수 있습니다. Linux 5.1~5.16에 영향을 미칩니다.

CVE-2023-2640 / CVE-2023-32629 (GameOver(lay)) — OverlayFS + User NS 권한 상승:

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 Namespace 보안 고려사항:

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.sockDocker 소켓 접근 차단
/proc/sysrq-triggerprocfs 마운트 + 쓰기 권한/proc를 readonly 마운트
cgroup release_agentcgroup v1 + CAP_SYS_ADMINcgroup v2 사용, seccomp으로 차단
nsenter 남용호스트 nsenter 바이너리 접근read-only rootfs, 불필요한 도구 제거
커널 모듈 로드CAP_SYS_MODULEcapability 제거, 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을 slaveprivate로 설정하여 불필요한 이벤트 전파를 줄입니다. 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
unshareEPERM으로 실패 user.max_*_namespaces가 0이거나, seccomp 차단 sysctl user.max_user_namespaces, dmesg sysctl 제한값 조정
setnsEINVAL로 실패 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 프로세스를 볼 수 있음
Pause 컨테이너의 역할:

쿠버네티스의 각 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)의 핵심 기술이며, 네임스페이스 상태를 정확히 보존하고 복원해야 합니다.

네임스페이스체크포인트 저장 내용복원 시 처리주요 과제
PIDPID 번호, 계층 구조clone3(set_tid)로 원래 PID 복원PID 충돌 회피
Network인터페이스, 라우팅, iptables, 열린 소켓netlink로 네트워크 재구성TCP 연결 상태 복원
Mount마운트 트리, propagation 유형순서대로 마운트 재구성외부 스토리지 의존성
Timemonotonic/boottime 오프셋CLONE_NEWTIME + 오프셋 설정Time NS 도입의 주 동기
UserUID/GID 매핑매핑 테이블 복원한 번만 쓸 수 있는 제약
IPCSysV 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 조회lsnsroot (전체 목록), 일반 (자기 것만)
특정 유형 NS 조회lsns -t netroot
프로세스의 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/bashroot 또는 User NS
새 PID+Mount NS 생성unshare --pid --fork --mount-proc /bin/bashroot
새 Net NS 생성ip netns add mynsroot
Net NS 진입ip netns exec myns commandroot
모든 NS 진입nsenter --target PID --all /bin/bashroot
특정 NS만 진입nsenter --target PID --net commandroot
NS bind mountmount --bind /proc/PID/ns/net /run/ns/mynetroot
NS bind mount 해제umount /run/ns/mynetroot
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=0root
Docker 컨테이너 PIDdocker inspect --format '{{.State.Pid}}' NAMEdocker 그룹
Docker 컨테이너 NS 진입nsenter --target $(docker inspect -f '{{.State.Pid}}' NAME) --net commandroot
veth 페어 생성ip link add veth0 type veth peer name veth1root
인터페이스 NS 이동ip link set veth1 netns mynsroot

커널 설정 옵션

네임스페이스 기능을 사용하려면 커널 빌드 시 해당 설정이 활성화되어 있어야 합니다. 대부분의 배포판 커널은 이미 활성화되어 있습니다.

커널 설정네임스페이스설명
CONFIG_NAMESPACES전체네임스페이스 인프라 (필수)
CONFIG_UTS_NSUTS호스트명 격리
CONFIG_IPC_NSIPCIPC 객체 격리
CONFIG_PID_NSPIDPID 격리
CONFIG_NET_NSNetwork네트워크 스택 격리
CONFIG_USER_NSUserUID/GID 매핑
CONFIG_CGROUP_NSCgroupcgroup 뷰 격리
CONFIG_TIME_NSTime시간 오프셋 격리
# 현재 커널의 네임스페이스 설정 확인
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

참고 링크

네임스페이스와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.