Container image security is four habits layered: scan for CVEs on every build, sign the image so consumers can verify provenance, pin base images by digest (not tag) so the build is reproducible, and run as non-root. The team that does all four ships images that pass a security audit; the team that does one of them has a partial defense. The tooling: Trivy, Grype, or Snyk for scanning; cosign for signing; digest-pinned Dockerfiles; USER directive in the Dockerfile.
Table of contents
- Layer 1: scan for CVEs
- Layer 2: sign the image
- Layer 3: pin base images by digest
- Layer 4: run as non-root
- Putting it together: a secure Dockerfile
- How this fits the rest of the stack
- FAQ
Layer 1: scan for CVEs
Scan on every build. Tools:
-
Trivy - the most popular, scans OS packages and language deps, free, fast.
-
Grype - from Anchore, similar coverage, good for CI.
-
Snyk - commercial but has a free tier, deep npm/Maven/PyPI coverage.
-
Docker Scout - built into Docker Hub, automatic on push.
In CI:
trivy image --severity HIGH,CRITICAL myapp:latest
Fail the build on HIGH or CRITICAL. The team that runs this in CI on every PR has a stream of CVE-driven PRs, which is the right thing - the team that has a clean image is a lie, and the audit will catch it.
Layer 2: sign the image
Signing is cryptographic proof that the image was built by who you think. Tools:
-
cosign (Sigstore) - the modern default, keyless with OIDC, or key-based.
-
Docker Content Trust (DCT) - the older Docker-native signing, uses Notary.
Sign after the build, push the signature:
cosign sign --key cosign.key myregistry/myapp:v1.0.0
Verify on pull:
cosign verify --key cosign.pub myregistry/myapp:v1.0.0
The team that signs images can prove to a customer or auditor that the image they are running is the one the team built. The team that does not sign is hoping no one tampers with the registry.
Layer 3: pin base images by digest
Tags are mutable - ubuntu:22.04 can change when Canonical updates. Digests are immutable - ubuntu@sha256:abc123... is one specific image. Pin by digest:
FROM ubuntu@sha256:7c75ab2b0567edbb9d48fc6c4a6f5f2ad2a8c5f6e1b2a3c4d5e6f7a8b9c0d1e2
The trade-off: you have to update the digest manually when you want a new base image. The team that automates this with Dependabot or Renovate gets the digest updates as PRs.
For internal base images, pin the digest. For a one-off dev build, the tag is fine. The team that has a production image always pins the digest.
Layer 4: run as non-root
Default: most container images run as root. The fix:
RUN adduser --system --uid 1001 myapp
USER myapp
Or in Kubernetes:
securityContext:
runAsNonRoot: true
runAsUser: 1001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
The team that runs as non-root in production has reduced the blast radius of a container escape. The team that runs as root accepts that any container escape gives the attacker root in the host.
Putting it together: a secure Dockerfile
FROM ubuntu@sha256:<digest>
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
RUN adduser --system --uid 1001 myapp
COPY --chown=myapp:myapp myapp /usr/local/bin/myapp
USER myapp
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/myapp"]
Pin by digest, minimal packages, non-root user, explicit entrypoint. The team that starts with this and adds as needed ships a clean image.
FAQ
What is the difference between scanning and signing?
Scanning checks for known vulnerabilities (CVEs) in the image’s contents. Signing proves the image was built by who you think. The team that scans but does not sign knows the image is clean but cannot prove it was built by them. The team that signs but does not scan has a provable but vulnerable image. Both are needed.
Do I really need to pin by digest?
Yes for production. A tagged image can be replaced upstream without you knowing. The team that pulls ubuntu:22.04 today and ubuntu:22.04 next month might get different content. Pinning by digest makes the build reproducible.
What is the most common container security mistake?
Running as root. The team that ships a Dockerfile without a USER directive has the default. The team that uses USER (or the equivalent in Kubernetes securityContext) has reduced the blast radius of a compromise.
Are Trivy and Snyk the same?
They overlap significantly. Trivy is free, Snyk has a free tier and a paid tier with deeper features. The team that is starting picks Trivy; the team that has a large portfolio and needs SBOM generation and policy-as-code picks Snyk.
Should I scan in CI or on registry push?
Both. CI scanning catches issues before merge. Registry scanning (Docker Scout, ECR image scanning) catches the image that is actually deployed. The team that does both has defense in depth.
How this fits the rest of the stack
For a sense of what the full project costs before it commits, the RunxBuild hosting calculator shows the line items together. The API, the database, the storage, the worker, the bandwidth - each one is a separate number, and the team’s mental model for the platform is the sum of those numbers.
Useful related references: