Skip to content
advanced Phase 5 · Docker Security

User Namespaces & Rootless

Run containers as non-root and use user namespace remapping.

45m
0 problems
Topic Progress 0%

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 --user flag 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