Chapter 11: Container and Virtualization Security

Updated

October 31, 2026

Chapter 11: Container and Virtualization Security

The migration from physical bare-metal hardware to containers and hypervisors transformed infrastructure delivery. However, abstraction layers introduce unique security boundaries and shared-resource risks.

Containers are not lightweight virtual machines; they are ordinary Linux processes executing on a shared host kernel, isolated solely by namespaces, cgroups, seccomp filters, and Linux Security Modules (LSM). Virtual machines provide hardware-enforced hypervisor boundaries, but introduce complex device emulation layers.

This chapter details the mechanisms required to secure containerized and virtualized workloads: container runtime hardening (containerd, Docker), Kubernetes node defense, container image supply chain integrity, hypervisor isolation (KVM, libvirt, sVirt), and defensive analysis of container breakout vectors.

Container Security Architecture

To secure containers, one must understand how the container isolation perimeter is constructed in the Linux kernel:

┌────────────────────────────────────────────────────────────────────────┐
│                      THE CONTAINER DEFENSE LAYERS                      │
│                                                                        │
│   Application Process (e.g., nodejs, python)                           │
│   ├─ User Identity: Runs as non-root UID 10001 (USER directive)        │
│   │                                                                    │
│   ▼                                                                    │
│   Layer 1: Capabilities & Primitives                                   │
│   ├─ Capability Bounding Set: Stripped of CAP_SYS_ADMIN, CAP_NET_ADMIN │
│   ├─ NoNewPrivileges = true: Blocks setuid execution                   │
│   │                                                                    │
│   ▼                                                                    │
│   Layer 2: Syscall Boundary                                            │
│   └─ Seccomp-BPF Profile: Blocks dangerous syscalls (ptrace, unshare)  │
│   │                                                                    │
│   ▼                                                                    │
│   Layer 3: Mandatory Access Control (LSM)                              │
│   └─ AppArmor / SELinux (sVirt/container_t): Confinements on paths     │
│   │                                                                    │
│   ▼                                                                    │
│   Layer 4: Virtualization & Isolation                                  │
│   └─ Namespaces (mnt, pid, net, ipc, uts, user) & cgroups v2 limits    │
│   │                                                                    │
│   ▼                                                                    │
│   Linux Host Kernel (Ring 0)                                           │
└────────────────────────────────────────────────────────────────────────┘

If any single layer is misconfigured—for example, granting --privileged or mounting the host filesystem—the isolation model collapses, granting the container process direct control over host hardware.

Docker and Containerd Hardening

Production container hosts should run hardened runtime configurations:

Hardening Docker Engine via /etc/docker/daemon.json

{
  "icc": false,
  "no-new-privileges": true,
  "userns-remap": "default",
  "log-driver": "journald",
  "live-restore": true,
  "userland-proxy": false,
  "seccomp-profile": "/etc/docker/hardened-seccomp.json",
  "storage-driver": "overlay2"
}
  • "icc": false: Disables inter-container communication on the default bridge network, preventing an exploited container from directly sniffing or attacking adjacent containers.
  • "no-new-privileges": true: Prevents processes inside containers from acquiring additional privileges via setuid binaries.
  • "userns-remap": "default": Remaps container UID 0 (root inside container) to an unprivileged subordinate UID (e.g., UID 100000) on the host. If an attacker escapes to the host, they arrive as an unprivileged nobody.

Rootless Containers with Podman

Where possible, eliminate the root-owned container daemon entirely by adopting rootless container engines like Podman:

# Launch a fully rootless container as an unprivileged user
podman run -d --name secure-web -p 8080:80 nginx:alpine

In rootless mode, the entire container engine, namespace creation, and filesystem mounts execute within the unprivileged user’s ambient authority.

Kubernetes Node Security

In a Kubernetes cluster, the Linux operating system serves as the node substrate. If a worker node’s operating system is compromised, the attacker can hijack all pods scheduled on that node and steal cluster service account secrets.

┌────────────────────────────────────────────────────────────────────────┐
│                        HARDENED KUBELET TOPOLOGY                       │
│                                                                        │
│   API Server ──► Webhook Authentication (x509 Client Certs)           │
│                         │                                              │
│                         ▼                                              │
│   Kubelet (Node Worker) ──► NodeRestriction Admission Controller       │
│   ├─ Anonymous Auth: Disabled (anonymous-auth: false)                  │
│   ├─ Read-Only Port: Disabled (readOnlyPort: 0)                        │
│   ├─ Protect Node PKI: /var/lib/kubelet/pki/ mode 0700 (root only)     │
│   └─ Enforce AppArmor Profile: runtime/default                         │
└────────────────────────────────────────────────────────────────────────┘

Kubelet Configuration Hardening

Apply the following settings in /var/lib/kubelet/config.yaml:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
  x509:
    clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
  mode: Webhook
readOnlyPort: 0
protectKernelDefaults: true
makeIPTablesParseable: true
eventRecordQPS: 50
  • protectKernelDefaults: true: Ensures kubelet exits with an error if required kernel security sysctls are not properly configured.

Container Image and Registry Security

Container security begins during the build phase, long before images are scheduled on nodes:

  1. Minimal Base Images: Avoid heavy OS base images (Ubuntu, Debian) containing package managers, compilers, and utilities (curl, nc, bash). Use scratch or Google’s distroless images containing only the compiled application binary and its direct runtime dependencies:

    FROM golang:1.24 AS builder
    WORKDIR /app
    COPY . .
    RUN CGO_ENABLED=0 go build -o server .
    
    FROM gcr.io/distroless/static:nonroot
    COPY --from=builder /app/server /server
    USER nonroot:nonroot
    ENTRYPOINT ["/server"]
  2. Automated Vulnerability Scanning: Integrate scanners (such as Trivy or Grype) into CI/CD pipelines to block images containing critical CVEs:

    trivy image --exit-code 1 --severity CRITICAL,HIGH mycompany/api:v1.2.0
  3. Cryptographic Image Signing with Cosign: Sign images upon successful build and enforce signature validation in Kubernetes admission webhooks (Kyverno or Gatekeeper):

    cosign sign --key k8s.key myregistry.corp.internal/mycompany/api:v1.2.0

Virtualization Host Hardening

For hypervisors running KVM and QEMU:

  • sVirt Isolation: libvirt integrates with SELinux and AppArmor via sVirt. Each virtual machine process (qemu-system-x86_64) is launched with a unique, randomly assigned MCS security label (e.g., svirt_t:s0:c123,c456). Even if an attacker escapes the guest VM via a QEMU device emulation bug, sVirt prevents the process from reading or modifying the disk images of other tenant VMs.
  • Dedicated Management Networks: Hypervisor management interfaces (SSH, libvirt RPC) must reside on isolated physical VLANs separated from guest VM tenant traffic.

Container Escapes and Privilege Boundaries

Understanding how containers break out is crucial for building resilient defenses:

1. The --privileged Escape

Running a container with --privileged disables all seccomp filters, drops no capabilities, and exposes all host device nodes (/dev) inside the container. An attacker escapes in three commands:

# Inside a privileged container:
mkdir /tmp/host_root
mount /dev/nvme0n1p2 /tmp/host_root
chroot /tmp/host_root
# Total root access over the host operating system achieved

2. The Docker Socket Mount Escape

Mounting /var/run/docker.sock inside a container allows the container to talk directly to the host Docker daemon API. The attacker simply instructs the daemon to spin up a new container with root filesystem mounts:

# Inside container with mounted docker.sock:
docker run -it -v /:/host-root ubuntu chroot /host-root

Defensive Rule: Never run containers with --privileged. Never mount /var/run/docker.sock. Use unprivileged containers, read-only root filesystems (--read-only), and user namespace remapping.