Skip to content

Cybersecurity · Container Security

Container Security in 2026: Image Scanning, SBOMs, and What Teams Actually Do

Running containers in production without scanning them is the equivalent of shipping code without running tests. Here's how teams scan images, generate SBOMs, and add runtime protection, from the CI step to the cluster.

Anurag Verma

Anurag Verma

7 min read

Container Security in 2026: Image Scanning, SBOMs, and What Teams Actually Do

Sponsored

Share

A container image is a packaged operating system layer, a language runtime, your application dependencies, and your code. Each of those layers can carry vulnerabilities. The OS base image might have an unpatched OpenSSL. Your Node.js version might have a known CVE. A transitive npm dependency might have a prototype pollution issue.

Without scanning, you ship those into production without knowing. With scanning, you catch them in CI before the image is deployed, or at least get a list of what you’re accepting risk on.

Image scanning has become a standard part of deployment pipelines in 2026. Here’s the practical setup.

Trivy: The Open Source Standard

Trivy (by Aqua Security) is the most widely adopted open-source image scanner. It’s fast, accurate, and handles more than containers:

  • Container images (Docker, OCI)
  • Filesystems and git repositories
  • Infrastructure as code (Terraform, Kubernetes manifests, CloudFormation)
  • SBOMs

Install it:

# macOS
brew install trivy

# Linux
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# Docker
docker run aquasec/trivy image <your-image>

Scan an image:

trivy image node:20-alpine

Output shows vulnerabilities by severity (CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN) with CVE IDs, affected package versions, and fixed versions when available.

node:20-alpine (alpine 3.19.1)
=================================
Total: 4 (CRITICAL: 0, HIGH: 2, MEDIUM: 2, LOW: 0)

┌──────────────┬────────────────┬──────────┬──────────────────────────────────┐
│   Library    │ Vulnerability  │ Severity │ Installed → Fixed Version        │
├──────────────┼────────────────┼──────────┼──────────────────────────────────┤
│ libssl3      │ CVE-2024-XXXX  │ HIGH     │ 3.1.4-r5 → 3.1.4-r6             │
│ libcrypto3   │ CVE-2024-XXXX  │ HIGH     │ 3.1.4-r5 → 3.1.4-r6             │
└──────────────┴────────────────┴──────────┴──────────────────────────────────┘

Integrating Into CI

The goal is to fail the build on critical and high severity vulnerabilities, with a policy for what to do about accepted exceptions.

# .github/workflows/build.yml
jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Scan image with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: table
          exit-code: '1'              # fail the build on findings
          severity: 'CRITICAL,HIGH'   # only block on these severities
          ignore-unfixed: true        # skip CVEs with no fix yet

ignore-unfixed: true is important. Many CVEs in base images have no available fix because the upstream maintainer hasn’t released a patch. Blocking on those means you can’t build until they do, which you can’t control. Focus enforcement on vulnerabilities where a fixed version exists.

For vulnerabilities you’ve reviewed and accepted (or that are false positives), Trivy supports a .trivyignore file:

# CVE-2024-12345 - affects crypto/tls, which we don't use in this service
# Reviewed 2026-03-01 by security team, accepted risk
CVE-2024-12345

# False positive - this package is not used at runtime
CVE-2024-67890

Keep this file in your repository and review it periodically. An ignore file that grows unchecked is a liability, not a policy.

Generating SBOMs

An SBOM (Software Bill of Materials) is a structured list of every component in your software: packages, libraries, their versions, and their licenses. The EU Cyber Resilience Act and US Executive Order 14028 both mandate SBOMs for software sold to government entities. Even if you’re not directly affected by those requirements now, your enterprise clients may start requiring SBOMs from their vendors.

Trivy generates SBOMs in CycloneDX or SPDX format:

# Generate SBOM in CycloneDX JSON format
trivy image --format cyclonedx --output sbom.json myapp:latest

# Generate in SPDX format
trivy image --format spdx-json --output sbom.spdx.json myapp:latest

Add SBOM generation to your CI pipeline alongside scanning:

- name: Generate SBOM
  run: |
    trivy image \
      --format cyclonedx \
      --output sbom-${{ github.sha }}.json \
      myapp:${{ github.sha }}

- name: Upload SBOM artifact
  uses: actions/upload-artifact@v4
  with:
    name: sbom-${{ github.sha }}
    path: sbom-${{ github.sha }}.json
    retention-days: 90

Store SBOMs alongside releases. When a new CVE drops, you can check your historical SBOMs to determine which deployed versions are affected without re-scanning everything.

Writing Smaller, Safer Images

Scanning tells you about vulnerabilities. Reducing attack surface prevents them. The two techniques that matter most:

Use distroless or Alpine base images. A standard Debian or Ubuntu base image includes hundreds of packages you don’t need, each a potential vulnerability source. Alpine is 5MB and includes almost nothing. Distroless images (from Google) include only the language runtime and your application, not a shell or package manager.

# Before: full Debian base (~200MB, many packages)
FROM node:20

# After: Alpine base (~50MB, minimal packages)
FROM node:20-alpine

# After: distroless (smallest attack surface, no shell)
FROM gcr.io/distroless/nodejs20-debian12

Distroless images have no shell, which means docker exec doesn’t work for debugging. Plan your observability before switching.

Multi-stage builds separate build tools from runtime images. Build tools (compilers, package managers, test frameworks) don’t belong in production images:

# Build stage — includes everything needed to build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build

# Runtime stage — only what's needed to run
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node         # don't run as root
EXPOSE 3000
CMD ["node", "dist/index.js"]

The USER node line matters: running as root in a container is a significant privilege escalation risk if the container is compromised.

Kubernetes-Level Policy

For teams running Kubernetes, image scanning in CI isn’t enough on its own. Nothing stops someone from deploying an unscanned image directly via kubectl. Admission controllers close this gap.

Kyverno (open source, CNCF) can enforce that all pods use images from approved registries and that images have been scanned:

# kyverno-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-registry
      match:
        resources:
          kinds: [Pod]
      validate:
        message: "Images must come from our approved registry"
        pattern:
          spec:
            containers:
              - image: "registry.company.com/*"
    - name: disallow-root
      match:
        resources:
          kinds: [Pod]
      validate:
        message: "Containers must not run as root"
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true

This blocks any Pod that tries to pull from Docker Hub or run as root from being scheduled.

Runtime Security With Falco

Image scanning is static analysis: it looks at what’s in the image before it runs. Runtime security monitors what containers actually do while running: which system calls they make, which files they access, which network connections they open.

Falco (CNCF project) runs as a privileged DaemonSet and detects anomalous behavior:

# Example Falco rules
- rule: Detect shell spawned in container
  desc: Container spawned a shell (unusual for most production apps)
  condition: >
    container and
    proc.name in (bash, sh, ash, zsh) and
    not proc.pname in (systemd, supervisor)
  output: "Shell spawned in container (container=%container.name user=%user.name)"
  priority: WARNING

- rule: Detect network tool in container
  desc: Network tool (curl, wget) running in production container
  condition: container and proc.name in (curl, wget, nc, nmap)
  output: "Network tool in container (container=%container.name command=%proc.cmdline)"
  priority: WARNING

These rules catch a common attack pattern: exploiting a web app to get code execution, then using that shell or curl to download a second-stage payload. A legitimate production Node.js app doesn’t spawn bash.

What Most Teams Actually Implement

In practice, the setup that covers most of the risk without becoming a full-time project:

  1. Trivy in CI on every build, blocking on CRITICAL/HIGH with fixes available
  2. .trivyignore for reviewed exceptions, reviewed quarterly
  3. Distroless or Alpine base images for all new services
  4. Multi-stage builds with USER nonroot
  5. SBOM generation stored as CI artifacts alongside releases

Runtime security (Falco) and admission controllers (Kyverno) are step two, worth adding once you have the image scanning baseline working. They’re more operationally complex to maintain and tune, and a team that does none of the above gains more from starting with CI scanning than from jumping to runtime monitoring.

Container security is one of those areas where 80% of the benefit comes from 20% of the work. Image scanning in CI is the 20%.

Sponsored

Enjoyed it? Pass it on.

Share this article.

Sponsored

The dispatch

Working notes from
the studio.

A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.

No spam, ever. Unsubscribe anytime.

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored