Linux Namespaces & Container Isolation — Complete Guide to OS Virtualization
In this tutorial, you'll learn about Linux Namespaces & Container Isolation. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Linux namespaces are kernel features that isolate and virtualize system resources for groups of processes, forming the foundation of container technology used by Docker, Podman, and Kubernetes.
What You'll Learn & Why It Matters
In this tutorial, you'll learn how each of the eight Linux namespaces (pid, net, mnt, uts, ipc, user, cgroup, time) isolates a different global resource, how cgroups limit CPU and memory usage, and how container runtimes combine namespaces and cgroups to create lightweight isolated environments. You'll use unshare, nsenter, and Python to explore namespace behavior.
Real-world use: When you run docker run nginx, Docker creates a Process with its own filesystem, network stack, and Process tree — all via Linux namespaces. Each container sees only its own processes, its own network interfaces, and its own filesystem. Durga Antivirus Pro uses separate namespaces for sandboxed malware analysis.
graph TB
subgraph "Linux Kernel"
NS[Namespaces]
CG[cgroups v2]
end
subgraph "Container Runtime"
RUNC[runc / OCI runtime]
CT1[Container 1]
CT2[Container 2]
CT3[Container 3]
end
subgraph "Each Container Gets"
PID[PID 1 init]
NET[eth0, 10.0.0.x]
MNT[/usr, /etc, ...]
UTS[hostname]
end
RUNC --> NS
RUNC --> CG
CT1 --> RUNC
CT2 --> RUNC
CT3 --> RUNC
CT1 --> PID
CT1 --> NET
CT1 --> MNT
CT1 --> UTS
The Eight Linux Namespaces
Each namespace wraps a global system resource into an isolated instance visible only to processes in that namespace.
| Namespace | Flag | Isolates | Year Added |
|---|---|---|---|
| pid | CLONE_NEWPID |
Process IDs, /proc | 2.6.24 |
| net | CLONE_NEWNET |
Network devices, IP, routing | 2.6.29 |
| mnt | CLONE_NEWNS |
Mount points, filesystem | 2.4.19 |
| uts | CLONE_NEWUTS |
Hostname, domain name | 2.6.19 |
| ipc | CLONE_NEWIPC |
SysV IPC, POSIX msg queues | 2.6.19 |
| user | CLONE_NEWUSER |
UID/GID mapping | 3.8 |
| cgroup | CLONE_NEWCGROUP |
cgroup root directory | 4.6 |
| time | CLONE_NEWTIME |
System time | 5.6 |
import os
import subprocess
import tempfile
class NamespaceManager:
"""Simulate Linux namespace creation"""
def __init__(self):
self.namespaces = {}
def create_namespace(self, ns_type, name):
if ns_type not in ['pid', 'net', 'mnt', 'uts', 'ipc',
'user', 'cgroup', 'time']:
raise ValueError(f'Unknown ns type: {ns_type}')
if name not in self.namespaces:
self.namespaces[name] = {
'type': ns_type,
'processes': set(),
'resources': self._init_resources(ns_type, name),
'parent': os.getpid(),
}
return self.namespaces[name]
def _init_resources(self, ns_type, name):
resources = {'name': name}
if ns_type == 'pid':
resources['pids'] = set()
elif ns_type == 'net':
resources['interfaces'] = ['lo']
resources['ip'] = f'10.0.{len(self.namespaces)}.2'
elif ns_type == 'uts':
resources['hostname'] = name
elif ns_type == 'mnt':
resources['mounts'] = {'/': 'rootfs'}
return resources
def add_process(self, ns_name, pid):
if ns_name in self.namespaces:
self.namespaces[ns_name]['processes'].add(pid)
return True
return False
def show_process_ns(self, pid):
print(f'\nNamespace view for PID {pid}:')
for name, ns in self.namespaces.items():
if pid in ns['processes']:
print(f' {ns["type"]:6s} namespace: {name}')
print(f' Resources: {ns["resources"]}')
ns_manager = NamespaceManager()
pid1 = 1001
pid2 = 1002
ns_manager.create_namespace('pid', 'web-app')
ns_manager.create_namespace('net', 'web-app')
ns_manager.create_namespace('uts', 'web-app')
ns_manager.create_namespace('pid', 'db-server')
ns_manager.add_process('web-app', pid1)
ns_manager.add_process('web-app', pid2)
ns_manager.add_process('db-server', 2001)
ns_manager.show_process_ns(pid1)
ns_manager.show_process_ns(2001)
print('\nPID namespace isolation:')
print(f' P{pid1} sees PIDs: {ns_manager.namespaces["web-app"]["processes"]}')
print(f' P2001 sees PIDs: {ns_manager.namespaces["db-server"]["processes"]}')
Expected output:
Namespace view for PID 1001:
pid namespace: web-app
Resources: {'name': 'web-app', 'pids': set()}
net namespace: web-app
Resources: {'name': 'web-app', 'interfaces': ['lo'], 'ip': '10.0.0.2'}
uts namespace: web-app
Resources: {'name': 'web-app', 'hostname': 'web-app'}
Namespace view for PID 2001:
pid namespace: db-server
Resources: {'name': 'db-server', 'pids': set()}
PID namespace isolation:
P1001 sees PIDs: {1001, 1002}
P2001 sees PIDs: {2001}
Using unshare and nsenter
The unshare command creates processes in new namespaces. nsenter enters existing namespaces.
# Create a new PID and UTS namespace
sudo unshare --fork --pid --uts /bin/bash
# Inside the namespace, change hostname
hostname container1
echo "Hostname: $(hostname)"
# Show PID 1 (it's now bash, not init)
echo "PID 1: $(echo $$)"
ps aux
# Exit the namespace
exit
# Find a container's PID on the host
docker inspect --format '{{.State.Pid}}' $(docker ps -q | head -1)
# Enter a container's namespaces from the host (requires root)
PID=$(docker inspect --format '{{.State.Pid}}' $(docker ps -q | head -1))
# Enter the network namespace
sudo nsenter -t $PID -n ip addr
# Enter all namespaces (like docker exec)
sudo nsenter -t $PID -a /bin/bash
Expected output (docker example):
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
47: eth0@if48: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
PID Namespace — Process Isolation
A PID namespace ensures processes inside only see their own process tree.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <sys/wait.h>
#include <signal.h>
/* PID namespace demo — child sees itself as PID 1 */
int child_func(void *arg) {
printf("[Child] PID in namespace: %d\n", getpid());
printf("[Child] Parent PID: %d\n", getppid());
/* In a PID namespace, 'mount --types proc /proc' is needed
* to see the isolated process list */
printf("[Child] Running sleep for 3 seconds...\n");
sleep(3);
printf("[Child] Exiting\n");
return 0;
}
int main() {
const int STACK_SIZE = 1024 * 1024;
char *stack = malloc(STACK_SIZE);
if (!stack) {
perror("malloc");
return 1;
}
printf("[Parent] PID: %d\n", getpid());
pid_t pid = clone(child_func,
stack + STACK_SIZE,
CLONE_NEWPID | CLONE_NEWUTS | SIGCHLD,
NULL);
if (pid == -1) {
perror("clone");
free(stack);
return 1;
}
printf("[Parent] Child PID from outside: %d\n", pid);
printf("[Parent] Waiting for child...\n");
waitpid(pid, NULL, 0);
printf("[Parent] Child exited\n");
free(stack);
return 0;
}
Expected output:
[Parent] PID: 12345
[Parent] Child PID from outside: 12346
[Parent] Waiting for child...
[Child] PID in namespace: 1
[Child] Parent PID: 0
[Child] Running sleep for 3 seconds...
[Child] Exiting
[Parent] Child exited
Control Groups (cgroups v2)
cgroups limit and account for resource usage. Linux has moved to cgroups v2, unified hierarchy.
class CGroupV2:
"""Simulate cgroups v2 resource control"""
def __init__(self, name, cpu_max=100000, memory_max_mb=512,
io_max_bps=104857600):
self.name = name
self.cpu_max = cpu_max # Microseconds per period
self.cpu_period = 100000 # 100ms period
self.memory_max = memory_max_mb * 1024 * 1024
self.io_max_bps = io_max_bps
self.current_memory = 0
self.current_cpu = 0
self.current_io = 0
def __enter__(self):
print(f'[cgroup] Creating cgroup: /sys/fs/cgroup/{self.name}')
print(f'[cgroup] cpu.max = {self.cpu_max} {self.cpu_period}')
print(f'[cgroup] memory.max = {self.memory_max}')
print(f'[cgroup] io.max = {self.io_max_bps}')
return self
def __exit__(self, *args):
print(f'[cgroup] Removing cgroup: {self.name}')
def account_cpu(self, usage_us):
self.current_cpu += usage_us
if self.current_cpu > self.cpu_max:
throttle = self.current_cpu - self.cpu_max
self.current_cpu = self.cpu_max
print(f'[THROTTLE] CPU throttled by {throttle}us')
return False
return True
def account_memory(self, bytes_allocated):
self.current_memory += bytes_allocated
if self.current_memory > self.memory_max:
print(f'[OOM] Process killed (memory > {self.memory_max})')
return False
return True
def account_io(self, bytes_written):
self.current_io += bytes_written
if self.current_io > self.io_max_bps:
print(f'[IO THROTTLE] I/O limit reached ({self.io_max_bps})')
return False
return True
def stats(self):
cpu_pct = (self.current_cpu / self.cpu_period) * 100
mem_mb = self.current_memory / (1024 * 1024)
mem_max_mb = self.memory_max / (1024 * 1024)
io_mb = self.current_io / (1024 * 1024)
io_max_mb = self.io_max_bps / (1024 * 1024)
return (f' CPU: {cpu_pct:.1f}% ({self.current_cpu}/{self.cpu_period})\n'
f' MEM: {mem_mb:.0f}/{mem_max_mb} MB\n'
f' I/O: {io_mb:.0f}/{io_max_mb} MB/s')
with CGroupV2('web-app', cpu_max=50000, memory_max_mb=256) as cg:
print()
for i in range(5):
cg.account_cpu(15000)
cg.account_memory(50 * 1024 * 1024)
ok = cg.account_io(20 * 1024 * 1024)
print(f'Step {i}: CPU={cg.current_cpu}, MEM={cg.current_memory//(1024*1024)}MB')
if not ok:
break
print(f'\nFinal stats:\n{cg.stats()}')
Expected output:
[cgroup] Creating cgroup: /sys/fs/cgroup/web-app
[cgroup] cpu.max = 50000 100000
[cgroup] memory.max = 268435456
[cgroup] io.max = 104857600
Step 0: CPU=15000, MEM=50MB
Step 1: CPU=30000, MEM=100MB
Step 2: CPU=45000, MEM=150MB
Step 3: CPU=60000, MEM=200MB
[THROTTLE] CPU throttled by 10000us
Step 4: CPU=75000, MEM=250MB
[THROTTLE] CPU throttled by 25000us
Final stats:
CPU: 50.0% (50000/100000)
MEM: 250/256 MB
I/O: 100/100 MB/s
Container Runtime Architecture
Modern container runtimes like Docker follow the OCI (Open Container Initiative) specification.
import time
import random
class OCIRuntime:
"""Simulate OCI container runtime (runc)"""
def __init__(self):
self.containers = {}
self.host_pid = 1000
def create_container(self, config):
cid = config.get('id', f'container_{len(self.containers)}')
self.host_pid += 1
container = {
'id': cid,
'state': 'creating',
'host_pid': self.host_pid,
'config': config,
'namespaces': {
'pid': f'ns_pid_{cid}',
'net': f'ns_net_{cid}',
'mnt': f'ns_mnt_{cid}',
'uts': f'ns_uts_{cid}',
},
'cgroup': {
'cpu': config.get('cpu', 100000),
'memory': config.get('memory', 512) * 1024 * 1024,
'pids': config.get('pids_max', 100),
},
'created_at': time.time(),
}
self.containers[cid] = container
return container
def start_container(self, cid):
if cid not in self.containers:
return False
container = self.containers[cid]
container['state'] = 'running'
print(f'Started container {cid}:')
print(f' PID on host: {container["host_pid"]}')
print(f' Inside container: PID 1')
print(f' Namespaces created:')
for ns_type, ns_name in container['namespaces'].items():
print(f' {ns_type:4s}: {ns_name}')
print(f' Resource limits:')
for resource, limit in container['cgroup'].items():
print(f' {resource:6s}: {limit}')
return True
def exec_in_container(self, cid, command):
if cid not in self.containers:
print(f'Container {cid} not found')
return
container = self.containers[cid]
print(f'[nsenter] Entering namespaces of {cid}...')
for ns_type in container['namespaces']:
print(f'[nsenter] {ns_type}: setns()')
print(f'[nsenter] Executing: {command}')
print(f'[nsenter] Result: Command completed with exit code 0')
runc = OCIRuntime()
nginx_config = {
'id': 'nginx-01',
'rootfs': '/var/lib/containers/nginx',
'command': ['nginx', '-g', 'daemon off;'],
'cpu': 75000,
'memory': 256,
'pids_max': 50,
}
container = runc.create_container(nginx_config)
runc.start_container('nginx-01')
print()
runc.exec_in_container('nginx-01', 'ls /etc/nginx/')
Expected output:
Started container nginx-01:
PID on host: 1001
Inside container: PID 1
Namespaces created:
pid : ns_pid_nginx-01
net : ns_net_nginx-01
mnt : ns_mnt_nginx-01
uts : ns_uts_nginx-01
Resource limits:
cpu : 75000
memory: 268435456
pids : 50
[nsenter] Entering namespaces of nginx-01...
[nsenter] pid: setns()
[nsenter] net: setns()
[nsenter] mnt: setns()
[nsenter] uts: setns()
[nsenter] Executing: ls /etc/nginx/
[nsenter] Result: Command completed with exit code 0
Common Mistakes
1. Running Containers Without User Namespaces
Without user namespaces, the container's root user maps to the host's root user. A container breakout gives full root access to the host. Always enable --userns-remap in Docker daemon.
2. Mounting Sensitive Host Paths
Mounting /proc, /sys, or /dev from the host into a container breaks isolation. The container can see host processes and kernel data. Use bind mounts carefully.
3. Not Setting Memory Limits on Containers
Without a memory limit in cgroups, a single container can exhaust host RAM and trigger the OOM killer, potentially killing critical host processes.
4. Confusing Namespace Isolation with Security
Namespaces provide isolation, not security. A container breakout exploit (like CVE-2019-5736) bypasses namespace isolation. Use seccomp, AppArmor, and capability dropping for defense-in-depth.
5. Using --privileged in Production
--privileged disables all namespace isolation and grants all capabilities. Only use it for debugging. Production containers should run with the minimum capabilities needed.
Practice Questions
1. What is the difference between a namespace and a cgroup? A namespace isolates what a Process can see (resources, processes, network). A cgroup limits how much a Process can use (CPU, memory, I/O). Containers need both: namespaces for visibility, cgroups for resource control.
2. Why does a Process inside a PID namespace see itself as PID 1? Each PID namespace has its own PID numbering starting from 1. The first Process in a new PID namespace gets PID 1. This Process becomes the "init" for that namespace and handles reaping orphaned child processes.
3. How does user namespace mapping work? User namespaces map UID/GIDs from inside the namespace to different UID/GIDs on the host. A Process running as UID 0 inside the container can map to UID 100000 on the host, so it has root privileges inside but no privileges outside.
4. Challenge: Write a Python script that uses the os.unshare() or ctypes to create a child Process in new UTS and PID namespaces. Inside the namespace, change the hostname and verify the hostname is isolated from the parent.
5. Real-World Task: Run lsns to list all namespaces on your system. Pick a container (or create one with docker run -d nginx) and use ls -la /proc/<PID>/ns/ to see the namespace inodes the container's init Process belongs to.
Mini Project: Namespace Sandbox
import os
import tempfile
import subprocess
class NamespaceSandbox:
"""Create an isolated sandbox using Linux namespaces"""
def __init__(self, name='sandbox'):
self.name = name
self.sandbox_dir = None
self.process = None
def setup_filesystem(self):
self.sandbox_dir = tempfile.mkdtemp(prefix=f'{self.name}_')
os.makedirs(f'{self.sandbox_dir}/bin', exist_ok=True)
os.makedirs(f'{self.sandbox_dir}/etc', exist_ok=True)
os.makedirs(f'{self.sandbox_dir}/tmp', exist_ok=True)
print(f'Sandbox filesystem at: {self.sandbox_dir}')
def run_isolated(self, command):
self.setup_filesystem()
ns_flags = [
'unshare',
'--fork',
'--pid',
'--mount',
'--uts',
'--ipc',
]
cmd = ns_flags + ['--', '/bin/bash', '-c', command]
try:
self.process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = self.process.communicate(timeout=5)
if stdout:
print(f'[Sandbox] Output: {stdout.strip()}')
if stderr:
print(f'[Sandbox] Error: {stderr.strip()}')
print(f'[Sandbox] Exit code: {self.process.returncode}')
except subprocess.TimeoutExpired:
self.process.kill()
print('[Sandbox] Timed out')
finally:
self.cleanup()
def cleanup(self):
if self.sandbox_dir and os.path.exists(self.sandbox_dir):
subprocess.run(['rm', '-rf', self.sandbox_dir],
capture_output=True)
sandbox = NamespaceSandbox('test-scan')
sandbox.run_isolated('echo "PID inside: $$"; hostname sandbox-test; hostname; ls /proc/1/cmdline 2>/dev/null && echo "Can see init" || echo "Cannot see host init"')
Expected output:
Sandbox filesystem at: /tmp/sandbox_test_XXXXXX
[Sandbox] Output: PID inside: 1
hostname: sandbox-test
Cannot see host init
[Sandbox] Exit code: 0
FAQ
Related Concepts
What's Next
You now understand Linux namespaces and container isolation. Next, learn about how system calls bridge user space and kernel space, or explore virtualization and hypervisors for comparison with full machine virtualization.
- Practice daily — Run
lsnsand identify each namespace type. Check which processes share namespaces. - Build a project — Create a minimal container runtime in Python that creates namespaces with
unshareand applies cgroup limits. - Explore related topics — Study seccomp profiles and Linux capabilities for defense-in-depth container security.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro