Container vulnerability scanners do their job well: they flag CVEs like a libcurl vulnerability buried three layers deep in your base image, match them against databases, and produce reports worth acting on. What they will never tell you is that your production workload is running as root with full host network access and an unrestricted syscall surface, because those are runtime properties that exist only after the container is already deployed, leaving a catastrophic posture gap behind a passing scan.

Teams frequently treat image scanning as the finish line for container security, and it is a reasonable starting point. Known CVEs should be caught before they ship. But the threat model for containers extends well beyond what static analysis of image layers can surface, and the gap between scanning and securing a container is where the interesting engineering lives.

Image Scanning Is Table Stakes

Vulnerability scanners like Trivy, Grype, and Snyk Container do valuable work: they decompose image layers, identify installed packages, and match them against CVE databases. This catches known vulnerabilities in dependencies you might not even realize you’re shipping. For compliance-driven environments, it is also a hard requirement.

Configuration drift compounds the problem. An image that was clean at build time can be running alongside misconfigured Kubernetes manifests that grant it capabilities the scanner never evaluated: privileged mode, host PID namespace access, writable root filesystems. These are the gaps that attackers exploit, and they live entirely outside the scanner’s field of view.

Hardening at Build Time

The cheapest security improvement in containerization is reducing the attack surface before anything runs. Distroless images from Google strip away package managers, shells, and debugging utilities that have no business existing in production. Scratch-based images for compiled languages like Go and Rust take this further: the container holds the binary and nothing else.

Multi-stage builds make this practical. Development stages carry compilers, linters, and test frameworks whilst the final stage copies only the compiled artifact into a minimal base. The image that ships has fewer packages, fewer tools for an attacker to leverage, and a significantly smaller CVE surface for scanners to worry about.

Generating a Software Bill of Materials (SBOM) at build time using tools like Syft creates a machine-readable inventory of everything in the image. This becomes foundational for downstream policy enforcement: admission controllers can require a valid SBOM before allowing deployment, and incident response teams can quickly determine whether a newly-disclosed CVE affects any running workload.

Admission Controllers as the Policy Gate

Build-time hardening only works if the runtime environment enforces it. Admission controllers sit at the Kubernetes API boundary and intercept every resource creation or modification request before it hits etcd. This is where organizational security policy becomes executable.

OPA/Gatekeeper and Kyverno both serve this function, albeit with different ergonomics. Gatekeeper uses Rego, a purpose-built policy language that is powerful but has a steep learning curve. Kyverno expresses policies as Kubernetes-native YAML, which lowers the adoption barrier for teams that already think in manifests.

The policies that matter most are the obvious ones that still get missed: blocking privileged containers, preventing host path mounts, requiring non-root user IDs, enforcing resource limits, and rejecting images from untrusted registries. Each of these is a one-line configuration mistake away from creating a cluster-wide escalation path.

Runtime Security and Syscall Filtering

Even with hardened images and admission policies, the container runtime itself exposes a broad kernel interface. Every container shares the host kernel, and the default syscall surface in Docker and containerd permits more than 300 syscalls, many of which most workloads will never use.

Seccomp (Secure Computing Mode) profiles restrict which syscalls a container can invoke. Docker ships a default profile that blocks several dozen of the most dangerous calls, including reboot, mount, and kexec_load. This is a useful baseline, albeit one that still permits ptrace, personality, and other calls that are rarely needed and regularly exploited.

Custom seccomp profiles for specific workloads deliver the largest attack surface reduction. A web server serving HTTP responses over a TLS socket has a well-defined syscall footprint. Profiling the application under load, capturing which syscalls it actually uses, and building a whitelist from that data produces a dramatically smaller attack surface than the default profile provides. That said, custom profiles carry a maintenance burden: every dependency update or feature change can introduce new syscall requirements, and a profile that blocks a newly-needed call will cause silent failures in production. For teams without dedicated kernel-level security expertise, RuntimeDefault is a reasonable compromise that provides meaningful protection without the ongoing upkeep.

Custom seccomp profiles built from real workload profiling can significantly reduce the permitted syscall set compared to the default Docker profile.

AppArmor and SELinux provide Mandatory Access Control at the kernel level, restricting file access, network operations, and capability usage independently of traditional Unix permissions. Read-only root filesystems prevent runtime modification of the container’s file tree, blocking a common class of persistence and payload-staging techniques. Combined, these controls create defense in depth that operates at a layer scanners will never reach.

Network Policies and Egress Control

Kubernetes does not enforce network policies unless the cluster runs a CNI plugin that supports them (such as Calico or Cilium), and even with a compatible CNI, no policies are applied by default, which means every pod can reach every other pod across every namespace. In a compromised container, this flat network topology turns a single breach into lateral movement across the entire cluster.

Default-deny ingress and egress policies at the namespace level are the starting point. From there, workload-specific policies should whitelist only the traffic patterns each service actually requires. A frontend pod needs to reach the API gateway; it has no reason to talk directly to the database or to make arbitrary outbound connections.

CNI plugins like Calico and Cilium enforce these policies at the kernel level using eBPF or iptables, and both provide visibility into actual traffic flows that can inform policy authoring. Writing network policies without observing real traffic patterns tends to produce either overly permissive rules that provide little value or overly restrictive rules that break application functionality.

Supply Chain Integrity

The final layer addresses a threat that image scanning cannot: how do you verify that the image running in production was actually built by your CI pipeline from the source code you control?

Image signing with cosign (part of the Sigstore project) attaches cryptographic signatures to container images. Admission controllers can then enforce that only signed images from trusted builders are permitted to deploy. This closes the gap where an attacker pushes a compromised image to your registry and deploys it through normal channels.

Provenance attestations go further, embedding machine-verifiable records of how, where, and from what source code an image was built. SLSA (Supply-chain Levels for Software Artifacts) provides a framework for grading these attestations, and tools like SLSA Verifier can validate them at admission time.

Putting It Together

Container security is a layered discipline where image scanning catches known CVEs, build hardening shrinks the available attack surface, admission controllers enforce policy at deploy time, runtime controls restrict kernel-level behavior, network policies contain blast radius, and supply chain integrity verifies provenance of the artifact itself. Each layer addresses a category of threat that the others structurally cannot.

The teams that do this well tend to adopt these controls incrementally, starting with admission policies that block the most dangerous misconfigurations, then layering in runtime controls as they develop the operational maturity to manage custom profiles. Trying to implement everything simultaneously creates a configuration burden that either overwhelms the team or gets bypassed through blanket exceptions that defeat the purpose.

The scanner is a reasonable place to start, and everything worth protecting lives in the layers that follow.