Safeguard
AI Security

How to Secure Container Software: A Practical Guide

Securing container software means controlling the whole chain, base image, dependencies, build, registry, and runtime, not just scanning the final image. Here is a working model for each layer.

Karan Patel
Platform Engineer
6 min read

To secure container software you have to control the entire chain that produces and runs the image, the base image you start from, the dependencies you install, the build process, the registry you push to, and the runtime you deploy into, because a weakness in any one layer undermines the others. Scanning the final image is necessary but nowhere near sufficient; by the time a vulnerability shows up in a scan of the built artifact, it has usually been baked in by a choice made several layers earlier.

Containers democratized deployment, and they also multiplied the surface. A single application image can pull in a Linux distribution, a language runtime, dozens of OS packages, and hundreds of application dependencies, most of which the developer never explicitly chose. Securing that means treating the image as a supply chain, not a black box.

Where does container risk actually come from?

Four layers, roughly in order of how much risk they contribute.

The base image is the biggest single lever. A FROM ubuntu:latest drags in a full distribution with a large package set and a correspondingly large CVE count, most of it irrelevant to your app. Starting from a minimal or distroless base cuts the vulnerability count dramatically because there is simply less software present to be vulnerable.

Application dependencies are the second layer. The packages your code imports, and their transitive dependencies, are where most exploitable, application-relevant vulnerabilities live. This is the same open source risk you would have outside a container; the container just packages it.

The build process is the third. A Dockerfile that runs as root, copies in secrets, or pulls unpinned dependencies at build time introduces risk that no image scan will fully catch. Reproducibility and provenance of the build matter here.

Runtime configuration is the fourth. An image can be perfectly clean and still be dangerous if it runs privileged, mounts the Docker socket, or has no resource limits. Runtime is where a compromised container becomes a compromised host.

How do I choose and harden a base image?

Start minimal. Distroless images (which contain only your application and its runtime dependencies, no shell, no package manager) and minimal bases like Alpine or the -slim variants of official images carry far fewer packages and therefore far fewer CVEs. The tradeoff is debuggability, no shell means no exec into a running container, so many teams use a distroless production image and a fuller image for local debugging.

Pin the base image by digest, not just tag, so a rebuild is reproducible and a mutable latest tag cannot silently change what you ship:

FROM python:3.12-slim@sha256:<digest>

Rebuild regularly. A base image pinned to a digest is reproducible but also frozen, so schedule rebuilds against updated bases to pick up upstream security fixes rather than letting an image ossify with known CVEs for months.

What does a hardened Dockerfile look like?

The build itself carries controls that scanning cannot retrofit. A few that matter most:

FROM python:3.12-slim@sha256:<digest> AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM gcr.io/distroless/python3-debian12
WORKDIR /app
COPY --from=build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . .
USER 1000
ENTRYPOINT ["python", "main.py"]

The multi-stage build keeps build tooling out of the final image. The USER 1000 line means the container does not run as root, which is the single highest-value runtime control, a non-root container that is compromised has far less it can do. Never copy secrets into an image layer, even a deleted layer persists in image history and can be extracted. Pass secrets at runtime through the orchestrator's secret mechanism instead.

How should scanning fit into the pipeline?

Scan at two points, and scope the findings honestly. Scan dependencies as they are declared, at pull-request time, so a vulnerable library is caught before it is baked into any image. Then scan the built image before it is pushed to the registry, so you have a complete picture including OS packages the base image contributed. Gate the push on the result: a critical, reachable vulnerability should block promotion, while a low-severity CVE in a package your code never invokes should not fail the build at 2 a.m.

The value of scanning the assembled image and the source dependencies together is that you see both the OS-layer CVEs (from the base image) and the application-layer CVEs (from your code's dependencies) in one place, mapped to the artifact you actually deploy. An SCA and image-scanning workflow that scopes findings to the final image, rather than flagging every CVE in every package the base image happens to contain, is what keeps the triage queue small enough to act on. Generating an SBOM for each image at build time gives you the inventory to answer "which of our images contain this newly disclosed CVE" without rescanning everything.

What about runtime?

Runtime is where good images go to die from bad configuration. Enforce a baseline in your orchestrator: run as non-root, drop all Linux capabilities and add back only what is needed, set a read-only root filesystem where possible, and never mount the Docker or containerd socket into a workload container. In Kubernetes, a securityContext with runAsNonRoot: true, readOnlyRootFilesystem: true, and allowPrivilegeEscalation: false codifies most of this, and admission policy can reject any workload that does not meet it. Set CPU and memory limits so a compromised or runaway container cannot starve its neighbors. These controls are cheap to apply and turn a container escape from a host compromise into a contained failure.

FAQ

Is scanning the container image enough to secure it?

No. Image scanning catches known CVEs in the assembled artifact, but it does not address build-time risks (running as root, embedded secrets, unpinned bases) or runtime configuration (privileged mode, mounted sockets, missing limits). Secure container software requires controls across the whole chain, with scanning as one layer.

Should I use distroless or Alpine base images?

Both cut the vulnerability surface versus a full distribution. Distroless is the most minimal, no shell or package manager, which is excellent for production but harder to debug. Alpine and slim variants are smaller than full bases while retaining a shell. Many teams run distroless in production and a fuller image for local debugging.

Why does running a container as non-root matter so much?

If a non-root container is compromised, the attacker inherits limited privileges and has a much harder path to escaping to the host. Running as root removes that barrier. Adding USER to your Dockerfile and enforcing runAsNonRoot in the orchestrator is the single highest-value runtime control.

How do I know which of my images are affected by a new CVE?

Generate an SBOM for each image at build time and store it. When a new CVE is disclosed, you query your SBOMs for the affected component rather than rescanning every image live. This is why SBOM generation belongs in the build pipeline, not as an afterthought.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.