Container security basics: images, scanning, and runtime
Most teams treat a container as a disposable sandbox: build it, run it, throw it away. The reality is less comfortable. A container shares the host kernel, and the image you run is a supply-chain artifact assembled from base layers and package repositories that may be years old. The common container failures — exposed credentials, vulnerable base images, containers running as root — are rarely exotic; they come from skipping the basics. This article covers the three layers where those basics live: image, scan, and runtime.
Treat the image as a supply chain artifact
The base image is the largest part of your attack surface, and the part you write least. Pin base images by digest rather than by mutable tag, so a rebuild does not silently pull a different base. Prefer minimal images: distroless variants and scratch-based builds remove package managers, shells, and compilers that an attacker could otherwise use. For internal images, publish an SBOM alongside each artifact and sign images so consumers can verify what they are actually pulling. A registry that enforces signed, scanned images changes the default from “trust whatever the tag points to” to “only run what we verified.”
Scan early, scan often
Vulnerability scanners compare the packages in an image against known CVE databases. Run them in CI on every build, and fail on critical findings in production images. Scanning has two honest limits: it only finds known vulnerabilities, and it only finds package-level ones. Custom application code, misconfiguration, and logic flaws are invisible to a CVE scan. An image scanned at build time goes stale the moment a new advisory is published, so rescan images that sit in your registry. Keep scanner output in the build record; a scan you cannot reproduce later is not much better than none.
Keep secrets out of image layers
Anything written into a layer stays in the image history forever, even if a later step deletes it. That includes ARG values, ENV variables, files copied in for a build step, and token-bearing config files such as .npmrc or pip.conf. Use multi-stage builds so build-time tooling and credentials never reach the final image, and pass secrets to the build with BuildKit secret mounts instead of embedding them. At run time, deliver secrets through the environment or a secret store rather than baking them into the image. A credential broker that injects secrets at startup achieves the same goal: the image stays clean, and the secret never enters a layer that gets scanned, copied, and shared.
Lock down the runtime
The image is only half of it. Containers run on a shared kernel, so runtime hardening matters as much as image hygiene. Run containers as a non-root user with USER in the Dockerfile or –user at run time. Mount the root filesystem read-only where the application allows it. Drop capabilities with –cap-drop=ALL and add back only what the process needs. Set no-new-privileges, apply a seccomp profile, and put an upper bound on memory and CPU. These measures do not stop a determined attacker, but they sharply reduce what a compromised process can reach: the host filesystem, other containers, and the kernel interfaces that escalate privilege.
Where credentials fit
If your containers handle API keys or tokens for agent workloads, apply the same discipline at run time as at build time. Deliver secrets at run time through a credential broker such as trustless (https://github.com/ikkun1222/trustless), which reads an existing password store and injects values into a subprocess environment without ever writing them to a layer or a log. Image hygiene, scanning, and runtime hardening form the baseline; keeping credentials out of the image closes the gap scanners cannot see.
Start with the three layers
Scan your oldest image, check what user your containers run as, and look for any secret that shipped inside a layer. Fix those three and most of the common container failures disappear.
Which image in your fleet was built longest ago, and when was it last scanned?