Container Security in 2026: The Threat Model, the Supply Chain, and a Pod That Can't Hurt You
A virtual machine has its own kernel and a hardware-enforced boundary; if a process escapes the guest OS it still has to defeat the hypervisor. A container has none of that. Every container on a host shares the host’s single Linux kernel, and “isolation” is just a set of kernel features — namespaces, cgroups, capabilities — that the kernel can be talked out of enforcing. That is the one fact the rest of container security follows from: a default container is not a security boundary, it is a packaging format with some isolation bolted on. The good news is that with a dozen specific settings you can make a container behave like a real boundary. The bad news is that almost nobody applies them, and the defaults are permissive. This is the practical version of getting it right.
The Threat Model: What Actually Goes Wrong
Container compromise happens at four layers, and a real defense addresses all of them rather than just scanning images and calling it done:
- The supply chain. The image you run was built from a base image, dozens of OS packages, and your application’s dependencies — any of which can carry a known CVE or, worse, a deliberately malicious package. Most real-world container incidents start here, not with an exotic kernel exploit.
- The image contents. A bloated image ships a shell, a package manager,
curl, and compilers — tools an attacker uses for the second stage of an intrusion. Every binary you don’t include is one they can’t use. - Runtime privilege. A container running as root, with the full Linux capability set, a writable root filesystem, and no seccomp profile, is one application bug away from being a foothold on the host. This is where a web-app RCE becomes a host compromise.
- The orchestrator. In Kubernetes, a permissive
securityContext, a missing NetworkPolicy, an over-scoped ServiceAccount, or ahostPathmount turns a single compromised pod into lateral movement across the cluster.
The throughline: defense in depth. Assume the application will be compromised, and make sure that the blast radius stops at the container. Everything below is about shrinking that radius.
Image Security: Minimal, Pinned, Scanned
Start from the smallest base you can
The original advice — “use Alpine instead of Ubuntu” — is half-right and now dated. Alpine is small but ships a shell and apk, and its musl libc occasionally causes subtle runtime bugs. The 2026 default for a security-conscious image is distroless (Google’s gcr.io/distroless) or a Chainguard / Wolfi image: these contain your app, its runtime, and CA certificates — and nothing else. No shell, no package manager, no curl. An attacker who lands an RCE in a distroless container finds no tools to pivot with.
|
|
Pin by digest, not by tag
FROM alpine:3.19 is a moving target — and a stale one; the current Alpine is well past 3.19. Worse, a tag like :latest or even :3.21 can be re-pushed, so the image you scanned is not guaranteed to be the image you deploy. Pin the digest for reproducibility and tamper-evidence, and bump it deliberately:
|
|
The trade-off is real: pinned digests don’t auto-update, so you need automation (Renovate, Dependabot) to open PRs when the base image gets security fixes. A pinned-but-never-updated image is worse than a floating tag — it freezes the CVEs in place. Pin for reproducibility; automate the bumps.
Scan, and fail the build on what matters
Scan every image in CI, and make critical findings block the merge — see CI/CD pipelines for where this gate lives.
|
|
--ignore-unfixed is the pragmatic flag: blocking on CVEs that have no available patch just teaches your team to bypass the scanner. Gate on what someone can actually fix.
The Supply Chain: Sign It, Verify It, Admit Only What You Trust
Scanning tells you what’s in an image. It does not tell you the image running in production is the one your pipeline built. Closing that gap is the supply-chain story, and in 2026 it is table stakes, not paranoia.
|
|
The verification has to be enforced, not advisory. In Kubernetes that means an admission controller — Kyverno or OPA Gatekeeper — that rejects any pod whose image isn’t signed by your pipeline’s identity:
|
|
This is what turns “we sign our images” into “an unsigned image physically cannot run here.” Without enforcement, signing is a checkbox that an attacker simply ignores.
Runtime Hardening: Drop Everything, Then Add Back
This is the highest-leverage and most-skipped layer. A container starts with root, ~14 Linux capabilities, a writable filesystem, and the runtime’s default seccomp profile. Strip all of it.
Default container Hardened container
---------------- ------------------
user: root (uid 0) -> user: 10001 (non-root)
capabilities: ~14 default -> capabilities: drop ALL
privilege escalation: yes -> no-new-privileges: true
filesystem: read-write -> read-only root + tmpfs for /tmp
seccomp: unconfined/RT -> RuntimeDefault (or custom)
/var/run/docker.sock -> never mounted
| Control | What it stops | Docker flag / Compose |
|---|---|---|
| Non-root user | Root-on-host if the kernel boundary is bypassed | USER 10001 in Dockerfile |
--cap-drop ALL |
Raw sockets, mount, ptrace, module loading | cap_drop: [ALL] |
--security-opt no-new-privileges |
setuid binaries escalating mid-exploit | security_opt: ["no-new-privileges:true"] |
| Read-only root FS | Attacker writing tools/persistence to disk | read_only: true + tmpfs |
seccomp RuntimeDefault |
~44 dangerous syscalls (keyctl, ptrace, etc.) | on by default; don’t set unconfined |
| Resource limits | A compromised or runaway container DoSing the host | mem_limit, cpus |
No privileged: true |
Full host access — this disables all isolation | never set it |
|
|
Two rules carry most of the weight here. Never mount the Docker socket (/var/run/docker.sock) into a container — it is root on the host, full stop; any container with it can start a privileged container and own the machine. And never run --privileged unless you genuinely understand that it disables namespacing, capability dropping, and device restrictions all at once. For an even stronger boundary on multi-tenant or hostile workloads, run rootless Podman/Docker (the daemon itself runs as a non-root user) or a sandboxed runtime like gVisor or Kata Containers, which give each container its own kernel surface at a modest performance cost.
Kubernetes: Pod Security That Actually Holds
Everything above maps to a Kubernetes securityContext, and Kubernetes adds the Pod Security Standards — three profiles (privileged, baseline, restricted) enforced by a built-in admission controller via a namespace label. Set the namespace to restricted and the API server rejects pods that don’t comply, so the guarantee doesn’t depend on every developer remembering the YAML:
|
|
A pod that passes restricted looks like this:
|
|
Then close the network. By default every pod in a cluster can talk to every other pod; a default-deny NetworkPolicy turns that off so a compromised pod can’t scan and pivot:
|
|
The other Kubernetes-specific traps: never mount hostPath, never set hostNetwork/hostPID/hostIPC (they erase the namespace boundary you’re paying for), and scope every ServiceAccount with least-privilege RBAC instead of leaving the default token mounted. Most of these are the same homelab-to-prod hardening steps covered in Kubernetes for the homelab; they just matter more when the cluster is multi-tenant.
Secrets and Build-Time Credentials
Secrets must never bake into an image — image layers are cached, shared, and trivially docker history-inspected, so a secret in a RUN step or ENV line is a secret published to anyone who pulls the image. Use BuildKit’s secret mounts so the credential is available during the build but never written to a layer:
|
|
At runtime, prefer mounted secret files over environment variables — env vars leak into child processes, crash dumps, and /proc/<pid>/environ. For anything beyond a single host, back the secrets with an external manager (Vault, cloud secret stores, or the External Secrets Operator on Kubernetes) rather than checking encrypted blobs into git. The full treatment is in secrets management; the container-specific rule is simply: the image is public, the env is semi-public, a mounted file from a real secret store is the floor.
Detection: Assume Something Gets Through
Prevention is never complete, so the last layer is runtime detection. Falco (a CNCF project) watches syscalls and flags container behavior that shouldn’t happen — a shell spawned inside a distroless container, a write to /etc, an outbound connection from a pod that should only receive traffic, an attempt to read /proc of another process. Those are exactly the signals of a post-exploitation stage:
|
|
Pair detection with the basics that make an incident investigable: centralized logs, immutable audit trails, and image provenance you can actually trace back through the SBOM and signatures you generated above.
Verdict
The single idea to hold onto is that a container is not a security boundary until you make it one. The kernel is shared, the defaults are permissive, and “it runs in a container” buys you essentially nothing against a determined attacker out of the box. What buys you safety is a short, specific list applied at every layer: build from distroless or Wolfi, pin by digest and automate the bumps, scan and gate in CI on fixable CVEs, sign with cosign and enforce verification with an admission controller, and at runtime drop all capabilities, run non-root, mount the root filesystem read-only, keep seccomp on, and never mount the Docker socket or run privileged. On Kubernetes, label your namespaces restricted, write the matching securityContext, and default-deny the network.
None of this is exotic, and none of it requires a commercial platform — it is a dozen settings and three CLI tools. The reason container breaches keep happening is not that the controls are hard; it is that the defaults are loose and the controls are optional, so teams ship the defaults. Treat the hardened configuration as the starting template for every service, gate it in CI so it can’t regress, and add Falco so you find out when something slips through anyway. Defense in depth is the whole game: assume the app gets popped, and make sure the blast radius stops at the container.
Sources
- NIST SP 800-190 — Application Container Security Guide
- Kubernetes — Pod Security Standards
- Kubernetes — Configure a Security Context for a Pod or Container
- Sigstore / cosign — signing and verifying container images
- Trivy — vulnerability and misconfiguration scanner
- Kyverno — verify image signatures
- Falco — runtime security and rules
- Docker — build secrets with BuildKit
- GoogleContainerTools — distroless base images
- CNCF — Software Supply Chain Security Best Practices
Comments