Introduction
Supply chain security starts before a workload runs in the cluster. If an image contains unnecessary tools, unclear provenance, or an unapproved tag, the runtime controls you learned earlier have more risk to contain. A minimal approved image should have a small build context, a clear base image, an explicit application entrypoint, useful metadata labels, and evidence that it can run in the target cluster.
In this lab, you will create a small local image build context, build the image inside the labex-v134 minikube profile, inspect the resulting image metadata, deploy it with imagePullPolicy: Never, and verify the runtime contents from Kubernetes. This lab keeps the scope practical: you are not scanning images yet, and you are not enforcing registry policy yet. You are learning the first supply-chain habit: build a small, named, inspectable image and prove what is inside it.
Create a Small Image Build Context
In this step, you will create the files that become the image. A build context is the directory sent to the image builder. Keeping the build context small matters because every extra file is a chance to copy something unnecessary into an image later.
Start in the lab workspace:
cd /home/labex/project/minimal-approved-image
Read the lab notes:
sed -n '1,160p' IMAGE_BUILD_NOTES.md
Confirm the CKS context and namespace:
kubectl config current-context | tee context-check.txt
kubectl get namespace cks-prep-image | tee namespace-check.txt
Create the application script. This script prints a readiness message, prints the runtime UID, and then sleeps so Kubernetes can keep the container running for inspection.
cat > app.sh <<'EOF'
#!/bin/sh
set -eu
echo "minimal approved image ready"
echo "runtime uid: $(id -u)"
sleep 300
EOF
chmod 0755 app.sh
Create the Dockerfile. busybox:1.36 is the small base image for this lab. The OCI-style labels record why this image is approved in the lab, and USER 65534:65534 makes the image run as a non-root numeric user by default.
cat > Dockerfile <<'EOF'
FROM busybox:1.36
LABEL org.opencontainers.image.title="minimal-approved-app"
LABEL org.opencontainers.image.source="local-cks-prep"
LABEL security.labex.io.approved="true"
WORKDIR /app
COPY app.sh /app/app.sh
RUN chmod 0555 /app/app.sh
USER 65534:65534
ENTRYPOINT ["/app/app.sh"]
EOF
Save the build context inventory and a checksum of the application script:
find . -maxdepth 1 -type f -printf '%f\n' | sort | tee build-context-files.txt
sha256sum app.sh | tee app-sha256.txt
sed -n '1,120p' Dockerfile | tee dockerfile-review.txt
Your directory will now contain Dockerfile, app.sh, the notes, and the evidence files you are saving. The important image boundary is the COPY line in the Dockerfile: only app.sh is copied into /app/app.sh inside the image.
Build the Image Inside the Minikube Profile
In this step, you will build the image in the labex-v134 minikube profile. Building it there matters because Kubernetes will run containers from the minikube node runtime. If you only build an image on the outer VM Docker daemon, the minikube node may not be able to see it.
Build the image with a clear local tag:
minikube -p labex-v134 image build -t labex/minimal-approved:1.0 .
Inspect the image inside the minikube profile. The first command confirms that the image exists in the node runtime. The second command saves key metadata, including the configured user, entrypoint, and labels.
minikube -p labex-v134 ssh -- 'docker image ls labex/minimal-approved:1.0' | tee built-image-list.txt
minikube -p labex-v134 ssh -- 'docker image inspect labex/minimal-approved:1.0' | tee built-image-inspect.json
Use jq to extract the fields that matter for approval evidence:
jq '.[0].Config | {User, Entrypoint, Labels}' built-image-inspect.json | tee built-image-approval.json
You should see the image tag in built-image-list.txt, User set to 65534:65534, Entrypoint set to /app/app.sh, and the approval label set to true.
Run the Approved Image in Kubernetes
In this step, you will run the local image in Kubernetes. The Pod uses imagePullPolicy: Never because the image was built inside the minikube profile rather than pushed to a registry. That setting tells Kubernetes to use the local node image and fail if it is not present.
Create the Pod manifest:
cat > approved-image-pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: approved-image-app
namespace: cks-prep-image
labels:
app: approved-image-app
supply-chain.labex.io/approved: "true"
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
containers:
- name: app
image: labex/minimal-approved:1.0
imagePullPolicy: Never
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
EOF
Apply the Pod and wait for it:
kubectl apply -f approved-image-pod.yaml
kubectl wait --for=condition=Ready pod/approved-image-app -n cks-prep-image --timeout=120s
Save the Pod logs and current Pod YAML:
kubectl logs approved-image-app -n cks-prep-image | tee approved-image-logs.txt
kubectl get pod approved-image-app -n cks-prep-image -o yaml | tee approved-image-pod.current.yaml
The logs should show minimal approved image ready and runtime uid: 65534. That proves Kubernetes is running the image entrypoint as the intended non-root user.
Verify the Runtime Contents
In this step, you will inspect the running container from Kubernetes. Image approval should not stop at a tag name. You want evidence that the runtime contains the expected application path and does not contain obvious extra files that were never meant to be part of the image.
Inspect the application directory:
kubectl exec -n cks-prep-image approved-image-app -- sh -c 'ls -la /app; test -f /app/app.sh; echo APP_PATH_OK' | tee runtime-app-path.txt
Check the runtime user and a small negative check for an unnecessary debug file:
kubectl exec -n cks-prep-image approved-image-app -- sh -c 'id -u; test ! -e /app/debug-tool && echo NO_DEBUG_TOOL' | tee runtime-content-check.txt
Save a compact final approval record that combines Kubernetes state with image metadata:
kubectl get pod approved-image-app -n cks-prep-image -o json | jq '{image: .spec.containers[0].image, imagePullPolicy: .spec.containers[0].imagePullPolicy, podSecurityContext: .spec.securityContext, containerSecurityContext: .spec.containers[0].securityContext}' | tee final-pod-approval.json
jq '.[0].Config | {User, Entrypoint, Labels}' built-image-inspect.json | tee final-image-approval.json
The final evidence should show the local approved image tag, imagePullPolicy: Never, non-root runtime settings, the /app/app.sh entrypoint, and the approval label.
Summary
You practiced the first supply-chain workflow in this CKS Prep course: building and proving a minimal approved image. You created a small build context, built labex/minimal-approved:1.0 inside the labex-v134 minikube profile, inspected image metadata and labels, deployed the local image with imagePullPolicy: Never, and verified the runtime user and application contents from Kubernetes. Later supply-chain labs will build on this foundation with manifest scanning, Helm output review, SBOM and checksum evidence, trusted registry controls, and build-secret cleanup.



