User Namespace Remapping
User Namespace Remapping
The Problem
By default, root inside a container (UID 0) maps to root on the host (UID 0). If a container escapes, the attacker has root access on the host.
Without remapping:
Container UID 0 → Host UID 0 (root!)
Container UID 1000 → Host UID 1000
User Namespace Remapping
User namespace remapping maps container UIDs to different host UIDs:
With remapping (subordinate ID 100000):
Container UID 0 → Host UID 100000
Container UID 1000 → Host UID 101000
Even if the container escapes, the attacker only has unprivileged access on the host.
Configure User Namespace Remapping
# 1. Create subordinate ID ranges
sudo useradd -r -s /bin/false dockremap
# Add subordinate UIDs and GIDs
echo "dockremap:100000:65536" | sudo tee /etc/subuid
echo "dockremap:100000:65536" | sudo tee /etc/subgid
# 2. Configure Docker daemon
sudo tee /etc/docker/daemon.json <<EOF
{
"userns-remap": "default"
}
EOF
# 3. Restart Docker
sudo systemctl restart docker
# 4. Verify
docker info | grep "Remap"
# WARNING: User namespace remapping is experimental
# WARNING: Using userns-remap will disable "--user" support
Verify Remapping
# Create container
docker run -it --rm alpine id
uid=0(root) gid=0(root)
# Check actual host UID
ps -o pid,user,comm -p $(docker inspect --format '{{.State.Pid}}' <container_id>)
# PID USER COMMAND
# 1234 100000 sh
# Container root (UID 0) is actually UID 100000 on host
Limitations
- Volumes owned by root inside the container may not be accessible
- Some applications expect UID 0 to have special privileges
- Build processes may fail if they need root during build
- Cannot use
--userflag with userns-remap
Rootless Docker
Rootless Docker
Rootless mode runs the entire Docker daemon as a non-root user, eliminating the need for a privileged daemon.
Install Rootless Docker
# Prerequisites
sudo apt-get install uidmap dbus-user-session
# Create user (if not exists)
useradd -m -s /bin/bash dockeruser
# Switch to the user
su - dockeruser
# Install rootless Docker
dockerd-rootless-setuptool.sh install
# Set environment
export PATH=/home/dockeruser/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
# Verify
docker info | grep -i rootless
# Rootless: true
Rootless vs Rootful
| Feature | Rootful | Rootless |
|---|---|---|
| Daemon user | root | Regular user |
| Port range | 1-65535 | 1024-65535 |
| Cgroups | Full control | Limited |
| Performance | Slightly better | Slightly slower |
| Security | Daemon has root | No root anywhere |
| Storage | /var/lib/docker | ~/.local/share/docker |
Rootless Limitations
# Cannot bind to ports < 1024
# Error: bind: permission denied
docker run -p 80:80 nginx # Fails!
# Must use ports >= 1024
docker run -p 8080:80 nginx # Works!
# No iptables manipulation (uses slirp4netns)
# Cgroups v2 may have limitations
Podman (Rootless Alternative)
Podman is daemonless and runs entirely as the current user:
# Install Podman
sudo apt-get install podman
# Run containers (no daemon needed!)
podman run -d --name web nginx:1.25
# Same CLI as Docker
podman ps
podman images
podman build -t myapp .
# Generate systemd unit files
podman generate systemd --new --name web
# Migrate from Docker
podman-compose up -d # Drop-in replacement
Podman vs Docker
| Feature | Docker | Podman |
|---|---|---|
| Architecture | Client-daemon | Daemonless |
| Root required | Yes (default) | No |
| Systemd integration | Limited | Native |
| Docker compatibility | Native | Near-complete |
| Image format | OCI | OCI (same) |
Advanced Security Hardening
Advanced Security Hardening
No New Privileges
Prevents processes from gaining additional privileges:
docker run --security-opt no-new-privileges:true myapp
# In docker-compose.yml
services:
api:
security_opt:
- no-new-privileges:true
Seccomp Profiles
# List allowed syscalls
docker run --rm alpine cat /proc/1/status | grep Seccomp
# Use restrictive profile
docker run --security-opt seccomp=strict.json myapp
# Custom profile example (deny mount syscall)
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["mount"],
"action": "SCMP_ACT_ERRNO"
}
]
}
AppArmor Profile
#include <tunables/global>
profile docker-custom flags=(attach_disconnected) {
# Deny mount
deny mount,
# Deny write to /proc
deny /proc/** w,
# Allow network access
network inet tcp,
network inet udp,
}
# Load profile
sudo apparmor_parser -r /etc/apparmor.d/docker-custom
# Use profile
docker run --security-opt apparmor=docker-custom myapp
Complete Hardened Container
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/node_modules /app/node_modules
COPY --from=builder /app/package.json /app/
EXPOSE 3000
USER nonroot:nonroot
CMD ["dist/server.js"]
# Run hardened
docker run -d \
--user 1001:1001 \
--cap-drop ALL \
--read-only \
--tmpfs /tmp \
--security-opt no-new-privileges:true \
--security-opt seccomp=strict.json \
--memory 512m \
--cpus 1.0 \
--network mynet \
myapp:latest