Skip to content

Container Security & Image Supply Chain: Hardening & Signing

Created: 2026-11-10 Updated: 2026-11-10

Management Summary: Deploying workloads within containers provides no intrinsic security guarantee. Containers share the exact same underlying Linux operating system kernel as the host: tampered base images, unpatched dependencies exposing critical CVEs, overprivileged root execution, and writable filesystems yield direct breakout pathways into host infrastructure. A resilient posture demands an uncompromising defense-in-depth framework across the entire lifecycle: from minimalist multi-stage and distroless builds to automated vulnerability scanning (Trivy, Grype) and Software Bills of Materials (SBOM via Syft), extending through cryptographic signing with Cosign and host policy enforcement (policy.json) that deterministic blocks unsigned artifacts at the OS boundary. This guide explores the container threat matrix, runtime hardening, hardened builds, SCA scanning, signing, and policy enforcement.

  • Container Threat Matrix


    Shared-kernel attack surfaces, namespace breakouts, kernel exploits (Dirty Pipe), and the operational hazards of mounted container sockets.

  • Runtime Hardening


    Enforcing mandatory isolation via privilege dropping, --cap-drop=ALL, --read-only root filesystems, and no-new-privileges.

  • Minimalist Base Images


    Radically collapsing exposed attack surfaces using multi-stage Containerfiles, static binaries, and distroless or scratch base layers.

  • CVE Scanning (Trivy & Grype)


    Static source and image vulnerability scanning inside CI/CD pipelines: deterministically blocking compromised builds before staging.

  • SBOM (Software Bill of Materials)


    Full inventory transparency across all runtime dependencies utilizing standardized formats (CycloneDX, SPDX).

  • Cryptographic Signing (Cosign)


    Cryptographically signing container artifacts via Sigstore/Cosign and verifying integrity on hosts using /etc/containers/policy.json.


1. Container Threat Matrix & Breakout Vectors (Runtime Threat Modeling)

A pervasive misconception asserts that “containers isolate like virtual machines.” Architecturally, the reality is fundamentally different:

  • Virtual Machines (Hypervisor Ring -1): Each VM runs its own independent, hardware-isolated operating system kernel. A kernel panic or exploit inside one guest VM remains strictly isolated from neighboring instances.
  • Linux Containers (Shared Kernel Ring 0): Containers are standard Linux processes isolated via kernel features (namespaces for visibility boundaries, cgroups for resource accounting, and LSM modules for access controls). All containers share the single, shared Linux kernel with the host!
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    subgraph Developer_Stage["1. Build & Engineering Phase"]
        Code["Source Code & Dependencies<br/>(Git / npm / pip)"] ==> Build["Multi-Stage Build<br/>(Distroless / Scratch Base)"]
        Build --> Scan["Automated Pipeline Scan<br/>(Trivy / Syft SBOM)"]
    end

    subgraph Registry_Stage["2. Signing & Registry Distribution"]
        Sign["Cryptographic Signing<br/>(cosign sign via Sigstore)"] ==> Registry["OCI Container Registry<br/>(Quay.io / Harbor / OCI)"]
    end

    subgraph Host_Runtime["3. Host Runtime Environment"]
        Policy["Host Policy Engine<br/>(/etc/containers/policy.json)"] ==> Run["Hardened Podman Runtime<br/>(Rootless / Read-Only / Cap-Drop)"]
    end

    Developer_Stage ==> Registry_Stage
    Registry_Stage ==> Host_Runtime

The Three Most Critical Breakout Vectors

  1. Kernel Vulnerabilities (e.g., Dirty Pipe CVE-2022-0847): If a process inside a container can invoke system calls targeting an unpatched kernel flaw on the host, it can breach namespace boundaries and overwrite arbitrary read-only host storage.
  2. Overprivileged Containers (--privileged): Running with --privileged disables nearly all kernel containment mechanisms. It exposes all host device nodes (/dev/) and assigns full superuser capabilities (CAP_SYS_ADMIN). Escaping to the host requires nothing more than mounting the host disk from within the container.
  3. Mounted Runtime Sockets (/var/run/docker.sock): Exposing the container runtime socket to a container (e.g., for CI runners or monitoring) grants unconstrained administrative root on the host, as the container can instruct the runtime API to launch arbitrary privileged host containers.

2. Uncompromising Runtime Hardening (Defense-in-Depth)

To mitigate container breakout vulnerabilities, runtime environments must strictly implement the Principle of Least Privilege.

1. Mandatory Privilege Dropping: Eliminating Root

A container process must never run as root (UID 0). Even though rootless Podman remaps container UID 0 to an unprivileged subUID, retaining root within the user namespace unnecessarily expands kernel attack surfaces.

Enforce an explicit unprivileged user identity across Containerfiles and launch commands:

# Launch with explicit unprivileged UID (e.g., 10001)
podman run --user 10001:10001 ...

2. Stripping Kernel Capabilities: --cap-drop=ALL

The Linux kernel splits traditional superuser permissions into 40+ POSIX capabilities. By default, container runtimes grant ~14 capabilities (such as CAP_NET_RAW and CAP_CHOWN). For production services (APIs, databases, web servers), the vast majority of these privileges are unnecessary.

Enterprise Baseline Rule: Strip all capabilities by default, re-attaching only the absolute minimum required:

# Example: Allow solely binding ports < 1024, stripping all other privileges
podman run --rm \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  docker.io/library/nginx:alpine

3. Preventing Privilege Escalation: no-new-privileges

Blocks processes inside the container from gaining elevated permissions via setuid or setgid binaries (neutralizing rogue setuid utilities):

podman run --security-opt=no-new-privileges:true ...

4. Read-Only Root Filesystems (--read-only)

When attackers achieve Remote Code Execution (RCE) in application code, their standard objective is downloading persistence toolchains or rootkits into /tmp or /usr/bin.

Enforcing a read-only rootfs renders this attack path impossible:

# Mount root filesystem read-only, supplying transient storage via dedicated tmpfs
podman run --rm \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --tmpfs /var/run:rw,noexec,nosuid,size=16m \
  docker.io/library/nginx:alpine

Note: Adding noexec to the tmpfs mount guarantees that even if binary data is written to /tmp, execution is blocked by the kernel.


3. Secure Container Builds: Multi-Stage, Distroless & Scratch

Monolithic baseline images (such as ubuntu:latest or debian:bookworm) present significant enterprise risk: they bundle package managers (apt), shells (/bin/bash, /bin/sh), Python, Perl, and hundreds of command-line binaries. Attackers exploit these pre-existing tools (Living off the Land) to advance lateral movement.

1. Multi-Stage Build Architecture

Multi-stage builds strictly decouple build tooling (compilers, build headers) from production delivery layers:

%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    subgraph BuildStage["1. Build Stage (Heavyweight Toolchain)"]
        Compiler["golang:1.22-alpine<br/>(Compiler, Git, Headers)"]
        Source["Compile Application<br/>(CGO_ENABLED=0 go build)"]
        Compiler --> Source
    end

    subgraph FinalStage["2. Production Stage (Minimalist Distroless)"]
        Artifact["Compiled Binary<br/>(Pure Static Executable)"]
        Distroless["gcr.io/distroless/static-debian12<br/>(No shell, no apt, no CLI utilities)"]
        Artifact --> Distroless
    end

    Source ==>|"COPY --from=builder"| Artifact

2. Hardened Production Containerfile: Go Microservice

Create Containerfile:

# Containerfile
# ==============================================================================
# Enterprise Multi-Stage Build: Maximum Security & Minimal Attack Surface
# ==============================================================================

# STAGE 1: Builder
FROM docker.io/library/golang:1.22-alpine AS builder

WORKDIR /src

# Pre-cache module dependencies
COPY go.mod go.sum ./
RUN go mod download

COPY . .

# Compile static binary without glibc dependencies
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-w -s" \
    -o /bin/app .

# ------------------------------------------------------------------------------
# STAGE 2: Minimalist Production Runtime (Distroless)
# Contains exclusively: CA certificates, /etc/passwd for "nonroot", and tzdata
FROM gcr.io/distroless/static-debian12:nonroot

# Unprivileged default user provided by distroless (UID 65532)
USER nonroot:nonroot

# Copy solely the compiled executable from build stage
COPY --from=builder --chown=nonroot:nonroot /bin/app /app

# Expose target network port
EXPOSE 8080

# Launch binary directly without shell interpreter wrapper
ENTRYPOINT ["/app"]

Advantages of Distroless and Scratch:

  • Absence of Shell Interpreters: Even upon successful arbitrary code execution, spawning an interactive reverse shell fails because /bin/sh does not exist.
  • No Package Managers: Fetching external payloads via curl, wget, or apt is structurally impossible.
  • Minimized CVE Footprint: Without installed operating system packages, vulnerability scanners report zero unpatched system packages.

4. Automated Vulnerability Scanning with Trivy & Grype

Prior to pushing container images to production registries, artifacts must undergo automated vulnerability auditing.

1. Installing Trivy

Trivy (by Aqua Security) represents the enterprise benchmark for comprehensive static security analysis:

# Install Trivy on Debian / Ubuntu
sudo apt-get install wget apt-transport-https gnupg lsb-release -y
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install trivy -y

2. Executing Image Scans

# Perform deep audit of local or remote container image
trivy image docker.io/library/nginx:alpine

3. CI/CD Integration: Deterministic Pipeline Enforcement

Automated pipelines must terminate when unpatched vulnerabilities graded HIGH or CRITICAL are detected:

# Exit code 1 enforces pipeline failure on high/critical findings
trivy image \
  --exit-code 1 \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  my-app:latest

Tip: Passing --ignore-unfixed suppresses vulnerabilities for which no vendor patch has been issued, avoiding pipeline blockage on unresolvable upstream findings.

4. Managing False Positives with .trivyignore

When a vulnerability is verified as non-exploitable in the target architecture, record it with justification in .trivyignore:

# .trivyignore
# Rationale: CLI utility uncalled by the production application runtime
CVE-2023-12345

5. Software Bill of Materials (SBOM) with Syft

A Software Bill of Materials (SBOM) serves as the digital inventory documenting every software dependency, library, and license bundled inside an image. Compliance mandates (including NIS2 and US Executive Order 14028) require verifiable SBOMs for mission-critical services.

1. Installing Syft

Syft (by Anchore) generates standardized, machine-readable SBOMs in CycloneDX and SPDX specifications:

# Install Syft binary via official installer
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin

2. Generating SBOMs in CycloneDX Format

# Export standardized CycloneDX JSON manifest
syft docker.io/library/nginx:alpine -o cyclonedx-json=nginx-sbom.json

# Inspect identified software components directly in console
syft docker.io/library/nginx:alpine

3. Offline Vulnerability Matching via Grype

A key advantage of SBOMs is offline vulnerability matching without re-scanning the container image itself. Grype evaluates stored SBOMs against live CVE feeds:

# Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sudo sh -s -- -b /usr/local/bin

# Scan previously exported SBOM for newly published CVEs
grype sbom:nginx-sbom.json

6. Cryptographic Image Signing & Verification with Cosign (Sigstore)

Even verified and hardened images remain vulnerable if hosts can arbitrarily pull unverified third-party images from untrusted registries.

Using Cosign (part of the Sigstore project), OCI container images are cryptographically signed, with signatures stored as standardized OCI artifacts directly alongside the image inside the registry.

1. Installing Cosign

# Install Cosign binary on Linux (amd64)
COSIGN_VERSION=$(curl -s https://api.github.com/repos/sigstore/cosign/releases/latest | grep 'tag_name' | cut -d\" -f4)
curl -LO "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64"
sudo install -m 0755 cosign-linux-amd64 /usr/local/bin/cosign
rm -f cosign-linux-amd64

2. Generating Key Pairs

# Generate private and public cryptographic key pair
cosign generate-key-pair
Generated files: * cosign.key: The private signing key (protected by passphrase, stored securely in CI/CD secrets). * cosign.pub: The public verification key (distributed to deployment hosts).

3. Signing OCI Images

Following the registry push:

# Sign the container image referencing its immutable digest
cosign sign --key cosign.key quay.io/enterprise/web-app:latest

4. Verifying Signatures on Production Hosts

# Verify cryptographic authenticity prior to deployment
cosign verify --key cosign.pub quay.io/enterprise/web-app:latest
When Cosign returns the validated JSON payload, image authenticity and origin are mathematically proven!


7. Enterprise Host Policy Enforcement (/etc/containers/policy.json)

The definitive defensive mechanism: configuring Podman at the operating system layer to refuse execution of unverified images.

Policies are enforced globally via /etc/containers/policy.json.

Production Configuration: Mandatory Signature Enforcement

Configure /etc/containers/policy.json:

{
  "default": [
    {
      "type": "reject"
    }
  ],
  "transports": {
    "docker": {
      "quay.io/enterprise": [
        {
          "type": "sigstoreSigned",
          "keyPath": "/etc/pki/containers/cosign.pub",
          "signedIdentity": {
            "type": "matchRepository"
          }
        }
      ],
      "docker.io/library": [
        {
          "type": "insecureAccept"
        }
      ]
    }
  }
}

Defensive Architecture:

  1. "default": [{"type": "reject"}]: Rejects image downloads from unknown registries unconditionally.
  2. "quay.io/enterprise": Internal organizational images are accepted only if validly signed by the public key /etc/pki/containers/cosign.pub.
  3. If an attacker or administrator attempts to pull an unsigned or altered image, Podman aborts the execution immediately:
    Error: Source image rejected: Signature verification failed
    

Integrating multi-stage distroless builds, automated pipeline SCA scanning (Trivy), SBOM auditing (Syft), cryptographic signing (Cosign), and host-level policy enforcement guarantees container supply-chain integrity at the highest enterprise standard.