A container image can include more than the application your team writes. It may contain a base operating system, runtime packages, third-party libraries, command-line tools, and other dependencies. If one of these components has a known vulnerability, the application may be affected even when its own source code has no obvious security issue.
This makes software supply chain security an important part of the build and deployment process.
A Software Bill of Materials (SBOM) helps teams understand what is inside a software artifact. Container image verification provides another layer of protection by checking whether an image comes from a trusted source and has not been altered unexpectedly.
Both practices can be integrated into a continuous integration and continuous delivery (CI/CD) pipeline. This article explains how to generate an SBOM, scan a container image, create a signed image, and verify that image before deployment.
What Is an SBOM?
An SBOM is a structured inventory of software components included in an application or artifact. Depending on the tool and image, it may identify operating system packages, application libraries, package versions, and other dependency details.
Consider a Docker image built for an ASP.NET Core application. It might contain:
Without an SBOM, investigating a newly reported vulnerability may require rebuilding the image or inspecting its layers manually. An SBOM gives security teams a starting point for identifying affected components.
Two widely used SBOM formats are SPDX and CycloneDX. Both provide machine-readable descriptions of software components, but their schemas and capabilities differ.
An SBOM is not a guarantee that software is secure. It may omit components that a tool cannot identify, and it does not automatically prove that an artifact is authentic. It should be used alongside vulnerability scanning, image signing, and deployment controls.
How Container Image Verification Works
Container security involves several separate checks.
First, build the image from a controlled source and identify its contents. Next, scan it for known vulnerabilities. Then generate an SBOM and, where appropriate, attach it to the image as an attestation. Finally, sign the image and verify its signature before deployment.
A typical workflow looks like this:
Build the container image.
Scan the image for known vulnerabilities.
Generate an SBOM.
Sign the image and create any required attestations.
Push the image to a container registry.
Verify the image signature and provenance before deployment.
These checks answer different questions. Vulnerability scanning looks for known security issues. An SBOM describes identified components. A signature helps establish the identity associated with an artifact. Provenance records information about how the artifact was built.
A valid signature does not mean an image has no vulnerabilities. Similarly, an image with no detected vulnerabilities is not necessarily authentic.
Step 1: Build a Container Image
Start with a Dockerfile that builds the application and produces a runtime image.
For example, a .NET application can use a multi-stage build:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o /app/publish \
--no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish ./
ENTRYPOINT ["dotnet", "MyApp.dll"]
Replace MyApp.dll with your application's actual assembly name.
The first stage uses the .NET SDK to compile and publish the application. The second stage uses the ASP.NET Core runtime image, so the final image does not need the full SDK.
For production builds, use an approved base image and review how image versions are managed. Pinning base images by digest can make builds more reproducible, but it also means you must deliberately update the digest to receive newer base image fixes.
Build the image locally or in CI:
docker build -t myapp:ci .
For a CI pipeline, use a unique image tag or digest to identify the artifact produced by that build. Avoid treating a mutable tag such as latest as a permanent identifier.
Step 2: Scan the Image for Vulnerabilities
Before publishing an image, scan it for known vulnerabilities.
Tools such as Trivy can inspect container images and report vulnerable operating system packages and application dependencies.
A basic scan looks like this:
trivy image myapp:ci
The output can include vulnerability identifiers, affected package versions, severity ratings, and available fixed versions.
You can also configure the scanner to return a nonzero exit code when it finds vulnerabilities at a chosen severity:
trivy image \
--severity HIGH,CRITICAL \
--exit-code 1 \
myapp:ci
This command fails when matching high- or critical-severity findings are reported.
A production pipeline should define how findings are handled. Some vulnerabilities may have no available fix, while others may not be reachable in the application's actual execution path. These cases still need review, but they should not automatically be treated as equivalent.
Avoid suppressing findings without a documented reason. If an exception is necessary, record the affected component, justification, owner, and review date.
Step 3: Generate an SBOM
After building the image, generate an SBOM for that exact image.
For example, Trivy can produce a CycloneDX document:
trivy image \
--format cyclonedx \
--output sbom.cdx.json \
myapp:ci
The generated sbom.cdx.json file contains the component information identified by the scanner.
You can keep this file as a CI artifact so developers and security teams can inspect it later. For release pipelines, associate the SBOM with the exact image digest rather than relying only on a tag that may be reused.
The distinction matters because a tag can point to a different image later. An SBOM generated for one image should not be assumed to describe every image that subsequently uses the same tag.
You should also decide how long to retain SBOM files and who can access them. They may reveal package versions and other details that are useful to attackers during reconnaissance.
Step 4: Create a Signed Image
Image signing helps establish that an image was signed by an expected identity. Cosign, part of the Sigstore project, can sign container images and verify those signatures.
For example, after pushing an image to a registry, capture its immutable digest:
docker push registry.example.com/myapp:1.0.0
docker inspect \
--format='{{index .RepoDigests 0}}' \
registry.example.com/myapp:1.0.0
The resulting repository digest identifies a particular image manifest. Use that digest for signing and verification rather than relying solely on the tag.
For a key-based Cosign workflow, a signing command can look like this:
cosign sign --key cosign.key \
registry.example.com/myapp@sha256:IMAGE_DIGEST
Replace IMAGE_DIGEST with the actual digest. The private key must be protected and should not be committed to source control.
This example demonstrates key-based signing. In GitHub Actions, keyless signing with a trusted OIDC identity may be preferable because it can avoid managing a long-lived signing key. The signing identity and verification policy must still be configured correctly.
Step 5: Verify the Image Before Deployment
A deployment pipeline should verify the image signature before allowing it to run in a production environment.
For a key-based signature, verification can look like this:
cosign verify \
--key cosign.pub \
registry.example.com/myapp@sha256:IMAGE_DIGEST
The command checks the signature against the supplied public key. Replace the digest and key path with the values used by your build process.
A successful verification establishes that the signature matches the image and the trusted public key. It does not establish that the image is free of vulnerabilities or that the build process itself was secure.
For keyless signing, verification should check the expected signing identity and certificate issuer. Do not accept any valid signature from an arbitrary identity. Define which repository, workflow, or identity is allowed to produce deployable artifacts.
Verification should happen before deployment, not only after the image has reached production.
Step 6: Integrate the Checks into GitHub Actions
The following workflow shows how to build an image, scan it, generate an SBOM, and retain the SBOM as an artifact.
This example focuses on image inspection. It does not yet publish or sign the image.
name: Container Security Checks
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
security:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Build container image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan container image
uses: aquasecurity/[email protected]
with:
image-ref: myapp:${{ github.sha }}
format: table
severity: HIGH,CRITICAL
exit-code: "1"
- name: Generate SBOM
run: |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$PWD:/work" \
aquasec/trivy:latest \
image \
--format cyclonedx \
--output /work/sbom.cdx.json \
myapp:${{ github.sha }}
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: container-sbom
path: sbom.cdx.json
if-no-files-found: error
The workflow builds the image from the checked-out source, scans it, generates the SBOM, and uploads the SBOM as a workflow artifact.
The example uses version tags to keep the configuration readable. In production, review third-party actions and pin them to verified full commit SHAs. Also pin scanner images to approved versions or digests instead of relying on latest.
For stronger isolation, consider a scanner installation method that does not require mounting the Docker socket. Access to that socket can provide extensive control over the host's Docker daemon, so it should not be granted casually.
To complete a release pipeline, add steps that push the image to a trusted registry, sign the image, and publish the SBOM or provenance attestation. A separate deployment job should verify the exact image digest and enforce the required vulnerability and signature policies.
Common Mistakes to Avoid
Generating an SBOM for the wrong image
An SBOM is useful only when it corresponds to the artifact being deployed. Generate it from the built image and associate it with the immutable digest.
Treating a successful scan as proof of security
Scanners detect known issues based on their vulnerability data and package identification. They cannot prove that an image has no security defects. Keep the scanner database updated and combine scanning with secure coding, testing, and configuration review.
Signing without verifying
Signing an image does not prevent an untrusted image from being deployed if the deployment process never checks the signature. Make verification a required deployment gate.
Trusting every valid signature
A valid signature from an unexpected identity should not be enough. Define the expected signer and, where available, verify the build provenance and repository identity.
Ignoring base image updates
A secure application can still run on an outdated operating system or runtime image. Rebuild images regularly, review base image updates, and rerun security checks after dependencies change.
Best Practices for Production CI/CD
Keep the security checks consistent across development, staging, and production. Use immutable image digests to identify artifacts, retain SBOMs with release records, and define clear rules for blocking deployments.
Use least-privilege permissions for registry access and deployment jobs. Separate pull-request validation from production deployment so untrusted code cannot obtain signing credentials or production permissions.
When possible, build an image once and promote the same digest between environments. Rebuilding separately for each environment can produce different artifacts, making verification and investigation more difficult.
Finally, decide who owns vulnerability exceptions and how quickly they must be reviewed. A pipeline that blocks releases without a process for handling legitimate exceptions can encourage developers to bypass security controls.
Conclusion
Generating an SBOM and verifying container images helps teams understand what they deploy and whether an artifact comes from an expected source.
A practical CI/CD pipeline builds the image, scans its components, generates an SBOM, signs the release artifact, and verifies its signature before deployment. Each step addresses a different risk, so none should be treated as a replacement for the others.
Start by generating an SBOM and scanning every build. Then introduce signing and verification for release images. As these checks become part of the normal deployment process, your team can investigate vulnerabilities more quickly and make image integrity a requirement rather than a manual review step.
Join the conversation! Your thoughts help the community grow.