Container Images Should Not Use The latest Or Untagged Tag
More Info:​
Verifies images are pinned to an immutable tag or digest. :latest and untagged images make deployments non-reproducible and hard to audit.
Risk Level​
Low
Address​
Security
Compliance Standards​
- Cloudanix Best Practice
Triage and Remediation​
- Remediation
Remediation​
Manual Steps
-
Identify noncompliant Pods and their owning controllers (run on any machine with kubectl access):
kubectl get pods --all-namespaces -o json | jq -r '[ .items[]| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)| .metadata as $m| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| ((.spec.containers // []) + (.spec.initContainers // []))[]| .image as $img| (($img | contains("@")) or (($img | split("/") | last | contains(":")) and (($img | endswith(":latest")) | not))) as $ok| select($ok | not)| "ns=\($m.namespace) pod=\($m.name) owner_kind=\($own.kind // "Pod") owner_name=\($own.name // $m.name) container=\(.name) image=\($img)"][]' -
For each listed controller (e.g., Deployment, StatefulSet, DaemonSet) or standalone Pod, export the current manifest (run on any machine with kubectl access). Example for a Deployment:
kubectl -n NAMESPACE get deployment OWNER_NAME -o yaml > /tmp/OWNER_NAME.yamlFor a standalone Pod:
kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/POD_NAME.yaml -
Edit the manifest locally to pin images to immutable tags or digests (run on the same machine where you saved the file):
vi /tmp/OWNER_NAME.yamlIn
spec.template.spec.containers[]andspec.template.spec.initContainers[], change each image from:image: myregistry.azurecr.io/app:latestor
image: myregistry.azurecr.io/appto a pinned reference, for example:
image: myregistry.azurecr.io/app:v1.2.3or
image: myregistry.azurecr.io/app@sha256:abcdef1234567890... -
Apply the updated manifest back to the cluster (run on any machine with kubectl access). Example:
kubectl apply -f /tmp/OWNER_NAME.yamlFor standalone Pods, delete and recreate them from the updated manifest:
kubectl delete -f /tmp/POD_NAME.yamlkubectl apply -f /tmp/POD_NAME.yaml -
Wait for new Pods to be ready using the pinned images (run on any machine with kubectl access). Example for a Deployment:
kubectl -n NAMESPACE rollout status deployment/OWNER_NAME -
Verify that no remaining Pods use
:latestor untagged images (run on any machine with kubectl access):kubectl get pods --all-namespaces -o json | jq -r '[ .items[]| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)| .metadata as $m| .metadata as $m| (.spec.nodeName // "") as $node| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| ((.spec.containers // []) + (.spec.initContainers // []))[]| .image as $img| (($img | contains("@")) or (($img | split("/") | last | contains(":")) and (($img | endswith(":latest")) | not))) as $ok| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)+ (if $node == "" then "" else " node=\($node)" end)+ (if $labels == "" then "" else " labels=\($labels)" end)+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)+ " container=\(.name) image=\($img)"+ " is_compliant=\(if $ok then "true" else "false" end)"] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'Confirm that all reported lines show
is_compliant=true.
Using kubectl
On any machine with kubectl access:
- Identify non‑compliant Pods (for context only; same as audit):
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| .image as $img
| (($img | contains("@")) or (($img | split("/") | last | contains(":")) and (($img | endswith(":latest")) | not))) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " container=\(.name) image=\($img)"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
- Export the owning workload manifest (Deployment/StatefulSet/Job, etc.), edit image tags, and re‑apply.
Example for a Pod owned by a Deployment:
# Replace NAMESPACE and DEPLOYMENT_NAME based on the "owner=" field from the audit
kubectl get deployment DEPLOYMENT_NAME \
-n NAMESPACE \
-o yaml > /tmp/deployment-DEPLOYMENT_NAME.yaml
Edit /tmp/deployment-DEPLOYMENT_NAME.yaml and, for each offending container under spec.template.spec.containers (and initContainers if present):
-
Replace an untagged image, e.g.:
image: myregistry.azurecr.io/app
with an immutable tag or digest, e.g.:image: myregistry.azurecr.io/app:v1.2.3orimage: myregistry.azurecr.io/app@sha256:abcdef...
-
Replace
:latest, e.g.:image: myregistry.azurecr.io/app:latest
with:image: myregistry.azurecr.io/app:v1.2.3orimage: myregistry.azurecr.io/app@sha256:abcdef...
Then apply:
kubectl apply -f /tmp/deployment-DEPLOYMENT_NAME.yaml
Repeat this export/edit/apply pattern for any other owning resources reported by the audit (e.g., StatefulSet, DaemonSet, Job, CronJob). For bare Pods without an owner reference, edit in place:
kubectl get pod POD_NAME -n NAMESPACE -o yaml > /tmp/pod-POD_NAME.yaml
# edit images in /tmp/pod-POD_NAME.yaml as above
kubectl delete pod POD_NAME -n NAMESPACE
kubectl apply -f /tmp/pod-POD_NAME.yaml
- Verification (on any machine with kubectl access):
After rollouts complete, re‑run the audit and confirm all lines show is_compliant=true (or only that single line):
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| .image as $img
| (($img | contains("@")) or (($img | split("/") | last | contains(":")) and (($img | endswith(":latest")) | not))) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " container=\(.name) image=\($img)"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Automation
#!/usr/bin/env bash
#
# Fix CBP C1.10 for Pods on AKS:
# - Detect running Pods outside system namespaces using :latest or untagged images
# - Patch the owning workload to use a pinned tag supplied via annotation
#
# IMPORTANT:
# - This script CANNOT guess the correct immutable tag or digest.
# - For each container image that should be pinned, you MUST set an annotation
# on the owning object with the desired immutable image.
#
# Supported owners: Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob
#
# Annotation format (on the owner object):
# c1-10.image-pins: |
# containerName1=registry.example.com/app@sha256:deadbeef...
# containerName2=myimage:1.2.3
#
# The script reads this annotation and updates .spec.template.spec.containers[*].image
# for containers listed there.
#
# Run on: any machine with kubectl access to the AKS cluster.
# Requirements: bash, kubectl, jq, yq (https://github.com/mikefarah/yq)
set -euo pipefail
command -v kubectl >/dev/null 2>&1 || { echo "kubectl not found in PATH"; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq not found in PATH"; exit 1; }
command -v yq >/dev/null 2>&1 || { echo "yq not found in PATH"; exit 1; }
# Namespaces to exclude from the check
EXCLUDED_NAMESPACES_REGEX='^(kube-system|kube-public|kube-node-lease)$'
echo "Scanning Pods for non-pinned images (excluding kube-system, kube-public, kube-node-lease)..."
# Build a JSON list of offending pods and their owners/containers/images
offenders_json="$(kubectl get pods --all-namespaces -o json | \
jq -c --arg re "$EXCLUDED_NAMESPACES_REGEX" '
.items[]
| select(.metadata.namespace | test($re) | not)
| . as $pod
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| . as $c
| .image as $img
| (($img | contains("@"))
or (($img | split("/") | last | contains(":"))
and (($img | endswith(":latest")) | not))
) as $ok
| select($ok | not)
| {
podNamespace: $pod.metadata.namespace,
podName: $pod.metadata.name,
containerName: .name,
image: $img,
owner: (
[($pod.metadata.ownerReferences // [])[] | select(.controller)] | first
)
}
')"
if [[ -z "$offenders_json" ]]; then
echo "No non-compliant Pod containers found."
exit 0
fi
echo "Found non-compliant containers:"
echo "$offenders_json" | jq -r '. | "ns=\(.podNamespace) pod=\(.podName) container=\(.containerName) image=\(.image) ownerKind=\(.owner.kind // "Pod") ownerName=\(.owner.name // .podName)"' | sort -u
echo
echo "Attempting to patch owning workload objects using c1-10.image-pins annotation..."
# Helper: get image pin for a container from owner annotation
get_pin_from_annotation() {
local ns="$1"
local kind="$2"
local name="$3"
local container="$4"
# Fetch annotation as multi-line "key=value"
local pins
pins="$(kubectl get "$kind" "$name" -n "$ns" -o jsonpath='{.metadata.annotations.c1-10\.image-pins}' 2>/dev/null || true)"
[[ -z "$pins" ]] && return 1
# Look for a line starting with "container="
# Trim CRLF and spaces
local line
while IFS= read -r line; do
line="${line%$'\r'}"
line="$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
[[ -z "$line" ]] && continue
if [[ "$line" == "$container="* ]]; then
echo "${line#*=}"
return 0
fi
done <<< "$pins"
return 1
}
# We will accumulate owners to patch to avoid multiple patches per owner
owners_to_patch_json="[]"
while IFS= read -r row; do
ns="$(echo "$row" | jq -r '.podNamespace')"
podName="$(echo "$row" | jq -r '.podName')"
containerName="$(echo "$row" | jq -r '.containerName')"
image="$(echo "$row" | jq -r '.image')"
ownerKind="$(echo "$row" | jq -r '.owner.kind // "Pod"')"
ownerName="$(echo "$row" | jq -r '.owner.name // .podName')"
if [[ "$ownerKind" == "Pod" ]]; then
echo "Skipping standalone Pod $ns/$podName (ownerKind=Pod). Pin image manually in its manifest or recreate from a pinned image."
continue
fi
# Normalize ownerKind to match kubectl resource types
case "$ownerKind" in
Deployment|StatefulSet|DaemonSet|ReplicaSet|Job|CronJob) ;;
*)
echo "Unsupported ownerKind $ownerKind for $ns/$podName; handle manually."
continue
;;
esac
# Try to get pin from annotation
pin="$(get_pin_from_annotation "$ns" "$ownerKind" "$ownerName" "$containerName" || true)"
if [[ -z "$pin" ]]; then
echo "No c1-10.image-pins entry found for container '$containerName' on $ownerKind/$ns/$ownerName; leaving unchanged. Add annotation and re-run."
continue
fi
echo "Will update $ownerKind/$ns/$ownerName container=$containerName image: $image -> $pin"
# Add to owners_to_patch_json, merging multiple containers per owner
owners_to_patch_json="$(jq -c \
--arg ns "$ns" \
--arg kind "$ownerKind" \
--arg name "$ownerName" \
--arg c "$containerName" \
--arg img "$pin" '
. as $root
| (map(select(.namespace == $ns and .kind == $kind and .name == $name)) | length) as $exists
| if $exists == 0 then
$root + [{
namespace: $ns,
kind: $kind,
name: $name,
containers: [{name: $c, image: $img}]
}]
else
map(
if .namespace == $ns and .kind == $kind and .name == $name then
.containers |= (
. as $cs
| (map(select(.name == $c)) | length) as $have
| if $have == 0 then
$cs + [{name: $c, image: $img}]
else
map(if .name == $c then .image = $img else . end)
end
)
else .
end
)
end
' <<< "$owners_to_patch_json")"
done <<< "$(echo "$offenders_json" | jq -c '.')"
if [[ "$owners_to_patch_json" == "[]" ]]; then
echo
echo "No owners had usable c1-10.image-pins annotations. Nothing patched."
echo "Add c1-10.image-pins annotations with immutable tags/digests and re-run."
exit 0
fi
echo
echo "Patching owning workload objects to use pinned images..."
echo "$owners_to_patch_json" | jq -c '.[]' | while IFS= read -r owner; do
ns="$(echo "$owner" | jq -r '.namespace')"
kind="$(echo "$owner" | jq -r '.kind')"
name="$(echo "$owner" | jq -r '.name')"
echo "Processing $kind/$ns/$name"
# Get current YAML
tmpfile="$(mktemp)"
kubectl get "$kind" "$name" -n "$ns" -o yaml > "$tmpfile"
# Build yq expression to update container images
yq_expr=""
while IFS= read -r c; do
cname="$(echo "$c" | jq -r '.name')"
cimg="$(echo "$c" | jq -r '.image')"
# Update both containers and initContainers if present
yq_expr+="
(.spec.template.spec.containers[] | select(.name == \"$cname\").image) = \"$cimg\" |
(.spec.template.spec.initContainers[]? | select(.name == \"$cname\").image) = \"$cimg\" |"
done <<< "$(echo "$owner" | jq -c '.containers[]')"
# Remove trailing pipe
yq_expr="${yq_expr%|}"
if [[ -z "$yq_expr" ]]; then
echo " No containers to update for $kind/$ns/$name"
rm -f "$tmpfile"
continue
fi
# Apply modifications
yq -y "$yq_expr" "$tmpfile" > "${tmpfile}.patched"
echo " Applying patched manifest..."
kubectl apply -f "${tmpfile}.patched"
rm -f "$tmpfile" "${tmpfile}.patched"
done
echo
echo "Waiting for Pods to roll out with new images..."
# Simple wait: let the controllers reconcile; users can improve with rollout status
sleep 10
echo
echo "Re-running compliance audit to verify..."
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| .image as $img
| (($img | contains("@")) or (($img | split("/") | last | contains(":")) and (($img | endswith(":latest")) | not))) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " container=\(.name) image=\($img)"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'