Base Image and SBOM Verification
If we want to allow only specific types of base images to be deployed,
we can save that information into the image attestation after obtaining it.
In Vulnerability Scanning and Verification, the cosign-vuln
format attestations already include the base image information.
But here we will use a different approach, using syft
to generate the SBOM for the image.
The SBOM information also includes the base image information.
In ACP (Alauda Container Platform), you can use trivy
or syft
task in Tekton Pipeline to generate the SBOM for image.
Here we use the syft task to generate SBOM.
TOC
Feature Overview
This method uses tools similar to syft to generate SBOM for the image and then use Kyverno to verify the SBOM:
- Use
syft
Tekton Task to generate SBOM for the image and attach to the image.
- Configure Kyverno rules to verify the SBOM.
- Use the image to create a Pod to verify the SBOM.
Use Cases
The following scenarios require referring to the guidance in this document:
- Implementing base image verification in Kubernetes clusters using Kyverno
- Enforcing security policies to only allow specific base images to be deployed
- Setting up automated SBOM generation and verification in CI/CD pipelines
- Ensuring base image compliance in production environments
- Implementing supply chain security controls for container images by verifying their base image information
Prerequisites
- A Kubernetes cluster with Tekton Pipelines, Tekton Chains and Kyverno installed
- A registry with image pushing enabled
kubectl
CLI installed and configured to access your cluster
cosign
CLI tool installed
jq
CLI tool installed
Process Overview
Step | Operation | Description |
---|
1 | Generate signing keys | Create a key pair for signing artifacts using cosign |
2 | Set up authentication | Configure registry credentials for image pushing |
3 | Configure Tekton Chains | Set up Chains to use OCI storage and configure signing, disable TaskRun SLSA Provenance |
4 | Create a sample pipeline | Create a pipeline definition with syft task to generate SBOM |
5 | Run a sample pipeline | Create and run a PipelineRun with proper configuration |
6 | Wait for signing | Wait for the PipelineRun to be signed by Chains |
7 | Get image information | Extract image URI and digest from the PipelineRun |
8 | (Optional) Get SBOM attestation | Get and verify the SBOM attestation |
9 | Verify with Kyverno | Create and apply Kyverno policy to verify base image information |
10 | Clean up | Delete test resources and policies |
Step-by-Step Instructions
Steps 1-3: Basic Setup
These steps are identical to the Quick Start: Signed Provenance guide. Please follow the instructions in that guide for:
Step 4: Create a Sample Pipeline
This is a Pipeline resource, which is used to build the image and generate the SBOM.
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: chains-demo-5
spec:
params:
- default: |-
echo "Generate a Dockerfile for building an image."
cat << 'EOF' > Dockerfile
FROM ubuntu:latest
ENV TIME=1
EOF
echo -e "\nDockerfile contents:"
echo "-------------------"
cat Dockerfile
echo "-------------------"
echo -e "\nDockerfile generated successfully!"
description: A script to generate a Dockerfile for building an image.
name: generate-dockerfile
type: string
- default: <registry>/test/chains/demo-5:latest
description: The target image address built
name: image
type: string
results:
- description: first image artifact output
name: first_image_ARTIFACT_OUTPUTS
type: object
value:
digest: $(tasks.build-image.results.IMAGE_DIGEST)
uri: $(tasks.build-image.results.IMAGE_URL)
tasks:
- name: generate-dockerfile
params:
- name: script
value: $(params.generate-dockerfile)
taskRef:
params:
- name: kind
value: task
- name: catalog
value: catalog
- name: name
value: run-script
- name: version
value: "0.1"
resolver: hub
timeout: 30m0s
workspaces:
- name: source
workspace: source
- name: build-image
params:
- name: IMAGES
value:
- $(params.image)
- name: TLS_VERIFY
value: "false"
runAfter:
- generate-dockerfile
taskRef:
params:
- name: kind
value: task
- name: catalog
value: catalog
- name: name
value: buildah
- name: version
value: "0.9"
resolver: hub
timeout: 30m0s
workspaces:
- name: source
workspace: source
- name: dockerconfig
workspace: dockerconfig
- name: syft-sbom
params:
- name: COMMAND
value: |-
set -x
mkdir -p .git
echo "Generate sbom.json"
syft scan $(tasks.build-image.results.IMAGE_URL)@$(tasks.build-image.results.IMAGE_DIGEST) -o cyclonedx-json=.git/sbom.json > /dev/null
echo -e "\n\n"
cat .git/sbom.json
echo -e "\n\n"
echo "Generate and Attestation sbom"
syft attest $(tasks.build-image.results.IMAGE_URL)@$(tasks.build-image.results.IMAGE_DIGEST) -o cyclonedx-json
runAfter:
- build-image
taskRef:
params:
- name: kind
value: task
- name: catalog
value: catalog
- name: name
value: syft
- name: version
value: "0.1"
resolver: hub
timeout: 30m0s
workspaces:
- name: source
workspace: source
- name: dockerconfig
workspace: dockerconfig
- name: signkey
workspace: signkey
workspaces:
- name: source
description: The workspace for source code.
- name: dockerconfig
description: The workspace for Docker configuration.
- name: signkey
description: The workspace for private keys and passwords used for image signatures.
TIP
This tutorial demonstrates a simplified workflow by generating the Dockerfile
and git-clone
task output inline within the pipeline.
In production environments, you would typically:
- Use the
git-clone
task to fetch source code from your repository
- Build the image using the Dockerfile that exists in your source code
- This approach ensures proper version control and maintains the separation between code and pipeline configuration
Explanation of YAML fields
- The same as in Step 4: Create a Sample Pipeline, but adds the following content:
workspaces
:
signkey
: The workspace for private keys and passwords used for image signatures.
tasks
:
syft-sbom
: The task to generate the SBOM for the image and upload the attestation.
Save into a yaml file named chains-demo-5.yaml
and apply it with:
$ export NAMESPACE=<default>
# create the pipeline in the namespace
$ kubectl create -n $NAMESPACE -f chains-demo-5.yaml
pipeline.tekton.dev/chains-demo-5 created
Step 5: Run a Sample Pipeline
This is a PipelineRun resource, which is used to run the pipeline.
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: chains-demo-5-
spec:
pipelineRef:
name: chains-demo-5
taskRunTemplate:
serviceAccountName: <default>
workspaces:
- name: dockerconfig
secret:
secretName: <registry-credentials>
- name: source
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: <nfs>
Explanation of YAML fields
- The same as in Step 5: Run a Sample Pipeline. Below only introduces the differences.
workspaces
signkey
: the secret name of the signing key.
secret.secretName
: The signing secret prepared in the previous step Get the signing secret. But you need to create a new secret with the same namespace as the pipeline run.
Save into a yaml file named chains-demo-5.pipelinerun.yaml
and apply it with:
$ export NAMESPACE=<default>
# create the pipeline run in the namespace
$ kubectl create -n $NAMESPACE -f chains-demo-5.pipelinerun.yaml
Wait for the PipelineRun to be completed.
$ kubectl get pipelinerun -n $NAMESPACE -w
chains-demo-5-<xxxxx> True Succeeded 2m 2m
Step 6: Wait for the PipelineRun to be signed
Wait for the PipelineRun has chains.tekton.dev/signed: "true"
annotation.
$ export NAMESPACE=<default>
$ export PIPELINERUN_NAME=<chains-demo-5-xxxxx>
$ kubectl get pipelinerun -n $NAMESPACE $PIPELINERUN_NAME -o yaml | grep "chains.tekton.dev/signed"
chains.tekton.dev/signed: "true"
Once the PipelineRun has chains.tekton.dev/signed: "true"
annotation, means the image is signed.
Step 7: Get the image from the PipelineRun
# Get the image URI
$ export IMAGE_URI=$(kubectl get pipelinerun -n $NAMESPACE $PIPELINERUN_NAME -o jsonpath='{.status.results[?(@.name=="first_image_ARTIFACT_OUTPUTS")].value.uri}')
# Get the image digest
$ export IMAGE_DIGEST=$(kubectl get pipelinerun -n $NAMESPACE $PIPELINERUN_NAME -o jsonpath='{.status.results[?(@.name=="first_image_ARTIFACT_OUTPUTS")].value.digest}')
# Combine the image URI and digest to form the full image reference
$ export IMAGE=$IMAGE_URI@$IMAGE_DIGEST
# Print the image reference
$ echo $IMAGE
<registry>/test/chains/demo-5:latest@sha256:a6c727554be7f9496e413a789663060cd2e62b3be083954188470a94b66239c7
This image will be used to verify the SBOM.
Step 8: (Optional) Get the SBOM attestation
TIP
If you interested about the SBOM attestation content, you can continue to read the following content.
More details about the cyclonedx SBOM attestation, please refer to cyclonedx SBOM attestation
Get the signing public key according to the Get the signing public key section.
# Disable tlog upload and enable private infrastructure
$ export COSIGN_TLOG_UPLOAD=false
$ export COSIGN_PRIVATE_INFRASTRUCTURE=true
$ export IMAGE=<<registry>/test/chains/demo-5:latest@sha256:a6c727554be7f9496e413a789663060cd2e62b3be083954188470a94b66239c7>
$ cosign verify-attestation --key cosign.pub --type cyclonedx $IMAGE | jq -r '.payload | @base64d' | jq -s
The output will be similar to the following, which contains the components information of the image.
cyclonedx SBOM attestation
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://cyclonedx.org/bom",
"predicate": {
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
"components": [
{
"bom-ref": "os:ubuntu@24.04",
"licenses": [
{
"license": {
"name": "GPL"
}
}
],
"description": "Ubuntu 24.04.2 LTS",
"name": "ubuntu",
"type": "operating-system",
"version": "24.04"
}
],
"metadata": {
"timestamp": "2025-06-07T09:56:05Z",
"tools": {
"components": [
{
"author": "anchore",
"name": "syft",
"type": "application",
"version": "1.23.1"
}
]
}
}
}
}
Description of the fields
predicateType
: The type of the predicate.
predicate
:
components
: The components of the image.
bom-ref
: The BOM reference of the component.
licenses
: The licenses of the component.
license
: The license of the component.
name
: The name of the license.
id
: The id of the license.
name
: The name of the component.
type
: The type of the component.
version
: The version of the component.
metadata
: The metadata of the image.
timestamp
: The timestamp of the image.
tools
:
components
: The components of the tool.
author
: The author of the tool.
name
: The name of the tool.
type
: The type of the tool.
version
: The version of the tool.
Step 9: Verify the base image information
Step 9.1: Create a Kyverno policy to verify the base image information
TIP
This step requires cluster administrator privileges.
More details about Kyverno ClusterPolicy, please refer to Kyverno ClusterPolicy
The policy is as follows:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-base-image
spec:
webhookConfiguration:
failurePolicy: Fail
timeoutSeconds: 30
background: false
rules:
- name: check-image
match:
any:
- resources:
kinds:
- Pod
namespaces:
- policy
verifyImages:
- imageReferences:
- "*"
# - "<registry>/test/*"
skipImageReferences:
- "ghcr.io/trusted/*"
failureAction: Enforce
verifyDigest: false
required: false
useCache: false
imageRegistryCredentials:
allowInsecureRegistry: true
secrets:
# The credential needs to exist in the namespace where kyverno is deployed
- registry-credentials
attestations:
- type: https://cyclonedx.org/bom
attestors:
- entries:
- attestor:
keys:
publicKeys: |- # <- The public key of the signer
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFZNGfYwn7+b4uSdEYLKjxWi3xtP3
UkR8hQvGrG25r0Ikoq0hI3/tr0m7ecvfM75TKh5jGAlLKSZUJpmCGaTToQ==
-----END PUBLIC KEY-----
ctlog:
ignoreSCT: true
rekor:
ignoreTlog: true
conditions:
- any:
- key: "{{ components[?type=='operating-system'] | [?name=='ubuntu' && (version=='22.04' || version=='24.04')] | length(@) }}"
operator: GreaterThan
value: 0
message: "The operating system must be Ubuntu 22.04 or 24.04, not {{ components[?type=='operating-system'].name[] }} {{ components[?type=='operating-system'].version[] }}"
- key: "{{ components[?type=='operating-system'] | [?name=='alpine' && (version=='3.18' || version=='3.20')] | length(@) }}"
operator: GreaterThan
value: 0
message: "The operating system must be Alpine 3.18 or 3.20, not {{ components[?type=='operating-system'].name[] }} {{ components[?type=='operating-system'].version[] }}"
PkgIDs: {{ scanner.result.Results[].Vulnerabilities[?CVSS.redhat.V3Score > `1.0`].PkgID[] }}.
Explanation of YAML fields
- The policy is largely consistent with the one in Image Signature Verification
spec.rules[0].verifyImages[].attestations[0].conditions
type
: The cyclonedx SBOM attestation type is https://cyclonedx.org/bom
attestors
: the same as above.
conditions
: The conditions to be verified.
any
: Any of the conditions must be met.
key: "{{ components[?type=='operating-system'] | [?name=='ubuntu' && (version=='22.04' || version=='24.04')] | length(@) }}"
: The operating system must be Ubuntu 22.04 or 24.04.
key: "{{ components[?type=='operating-system'] | [?name=='alpine' && (version=='3.18' || version=='3.20')] | length(@) }}"
: The operating system must be Alpine 3.18 or 3.20.
Save the policy to a yaml file named kyverno.verify-base-image.yaml
and apply it with:
$ kubectl create -f kyverno.verify-base-image.yaml
clusterpolicy.kyverno.io/verify-base-image created
Step 9.2: Verify the policy
In the policy
namespace where the policy is defined, create a Pod to verify the policy.
Use the built image to create a Pod.
$ export NAMESPACE=<policy>
$ export IMAGE=<<registry>/test/chains/demo-5:latest@sha256:a6c727554be7f9496e413a789663060cd2e62b3be083954188470a94b66239c7>
$ kubectl run -n $NAMESPACE base-image --image=${IMAGE} -- sleep 3600
If your base image is Ubuntu 22.04 or 24.04, the Pod will be created successfully.
Change the conditions in the ClusterPolicy
to only allow Alpine 3.18 or 3.20.
conditions:
- any:
- key: "{{ components[?type=='operating-system'] | [?name=='alpine' && (version=='3.18' || version=='3.20')] | length(@) }}"
operator: GreaterThan
value: 0
message: "The operating system must be Alpine 3.18 or 3.20, not {{ components[?type=='operating-system'].name[] }} {{ components[?type=='operating-system'].version[] }}"
Then create a Pod to verify the policy.
$ kubectl run -n $NAMESPACE deny-base-image --image=${IMAGE} -- sleep 3600
Receive the output like this:
Error from server: admission webhook "mutate.kyverno.svc-fail" denied the request:
resource Pod/policy/deny-base-image was blocked due to the following policies
verify-base-image:
check-image: 'image attestations verification failed, verifiedCount: 0, requiredCount:
1, error: .attestations[0].attestors[0].entries[0].keys: attestation checks failed
for <registry>/test/chains/demo-5:latest and predicate https://cyclonedx.org/bom:
The operating system must be Alpine 3.18 or 3.20, not ["ubuntu"] ["24.04"]'
Step 10: Clean up the resources
Delete the Pods created in the previous steps.
$ export NAMESPACE=<policy>
$ kubectl delete pod -n $NAMESPACE base-image
Delete the policy.
$ kubectl delete clusterpolicy verify-base-image
Expected Results
After completing this guide:
- You have a working setup with Tekton Chains for SBOM generation and Kyverno for base image verification
- Your container images automatically include SBOM information in their attestations
- Only images with acceptable base images can be deployed in the specified namespace
- Images with non-compliant base images are automatically blocked by Kyverno policies
- You have implemented a basic supply chain security control by verifying the base image information of your container images
This guide provides a foundation for implementing supply chain security in your CI/CD pipelines. In a production environment, you should:
- Configure proper namespace isolation and access controls
- Implement secure key management for signing keys
- Set up monitoring and alerting for policy violations
- Regularly rotate signing keys and update security policies
References