Minimize Cluster Access Read Only Azure Container Registry
More Info:
Scan images being deployed to Amazon EKS for vulnerabilities.
Risk Level
Medium
Address
Security
Compliance Standards
- APRA CPS 234 (Australia)
- BSI C5 (Germany)
- Brazil LGPD
- CCPA / CPRA (California)
- CIS AKS
- CIS Critical Security Controls v8
- CMMC 2.0
- CSA Cloud Controls Matrix v4
- DPDPA
- Digital Operational Resilience Act (EU)
- Essential 8
- ISO/IEC 27017
- ISO/IEC 27018
- ISO/IEC 27701
- KSA PDPL
- MAS Technology Risk Management (Singapore)
- MITRE ATT&CK (Cloud)
- NIS2 Directive
- NIST SP 800-171
- NYDFS 23 NYCRR 500
- SWIFT Customer Security Controls Framework
- Sarbanes-Oxley IT General Controls
- UK NCSC Cyber Assessment Framework
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
On any machine with Azure CLI access, list the ACRs the cluster uses (replace the resource group/name if known, or list all and filter by name pattern):
az acr list --output tableIdentify which registries are referenced in your Kubernetes image references (e.g.,
myregistry.azurecr.io/...). -
For each identified ACR, inspect the role assignments on the registry itself and on its resource group, focusing on your AKS-managed identities (node pool/cluster). First get the principal IDs for those identities:
# List AKS clusters and show their managed identitiesaz aks list --query '[].{name:name, rg:resourceGroup, MI:identity}' -o json# For a specific cluster (fill in names):az aks show \--resource-group RG_NAME \--name AKS_NAME \--query '{kubeletId:identityProfile.kubeletidentity.clientId, \clusterId:identity.principalId}' \-o jsonThen list role assignments on each ACR:
az role assignment list \--scope $(az acr show --name ACR_NAME --query id -o tsv) \--output json -
Review the role assignments from step 2 and determine whether any AKS identities have write-capable roles on the ACR, such as:
AcrPushOwnerContributor- Any custom role that includes
Microsoft.ContainerRegistry/registries/push/*,write, ordeleteactions.
Keep read-only roles (e.g.,AcrPullor custom roles limited to pull/list) and flag any broader roles for correction.
-
If an AKS identity has more than read-only access on an ACR, change it to read-only. First, remove the existing write-capable role assignment (substitute the actual principalId/role/scope from step 2):
az role assignment delete \--assignee PRINCIPAL_ID \--role "AcrPush" \--scope $(az acr show --name ACR_NAME --query id -o tsv)Then grant read-only pull access:
az role assignment create \--assignee PRINCIPAL_ID \--role "AcrPull" \--scope $(az acr show --name ACR_NAME --query id -o tsv) -
If you use service principals or workload identities (e.g., for CI/CD or image promotion) that must push images, ensure those identities—not AKS node/cluster identities—are the ones with
AcrPush/write roles. Confirm by listing their assignments:az role assignment list \--assignee PRINCIPAL_OR_APP_ID \--output jsonAdjust roles so only non-cluster identities used for image publishing retain push/write permissions.
-
Verify that cluster identities now have read-only access only by re-running the role assignment check and confirming no AKS-related principal has write-capable roles on any ACR:
az role assignment list \--scope $(az acr show --name ACR_NAME --query id -o tsv) \--output tableConfirm that AKS kubelet/cluster identities are assigned only
AcrPull(or equivalent custom read-only) roles.
Using kubectl
kubectl cannot be used to change Azure Container Registry permissions or scope; this control is enforced via Azure RBAC/ACR configuration on the cloud provider side. Make the required changes through Azure Portal/CLI/IaC as described, and follow the guidance in the Manual Steps section.
Automation
#!/usr/bin/env bash
#
# Purpose:
# Report how AKS accesses Azure Container Registries (ACR) so you can
# review whether the cluster has read‑only or read‑write rights.
#
# Requirements (run from any machine with Azure CLI and kubectl access):
# - az CLI authenticated to the subscription(s) hosting the AKS + ACR
# - kubectl context set to the target AKS cluster
#
# NOTE:
# This script only reports. It does NOT modify any role assignments.
set -euo pipefail
echo "=== 1) Discover AKS cluster identity (managed + kubelet) ==="
# Try both managed identity (newer AKS) and SP (legacy)
echo
echo "kubectl context: $(kubectl config current-context || echo 'N/A')"
# Get AKS cluster resource group and name from current context (best-effort)
# Adjust these manually if discovery fails.
CLUSTER_RESOURCE_GROUP="${CLUSTER_RESOURCE_GROUP:-}"
CLUSTER_NAME="${CLUSTER_NAME:-}"
if [[ -z "${CLUSTER_RESOURCE_GROUP}" || -z "${CLUSTER_NAME}" ]]; then
echo
echo ">> Auto-discovery of AKS name/RG (best-effort, may fail)"
# Try extracting from AKS node resource group labels
NODE_RG=$(kubectl get nodes -o jsonpath='{.items[0].metadata.labels.azure\.com/resource-group}' 2>/dev/null || true)
if [[ -n "${NODE_RG}" ]]; then
echo "Detected node resource group: ${NODE_RG}"
fi
echo "Set CLUSTER_RESOURCE_GROUP and CLUSTER_NAME in env for precise results."
fi
echo
echo ">> AKS kubelet identity (used to pull images) from nodes:"
kubectl get nodes -o wide --show-labels | sed 's/,/\n /g'
echo
echo "=== 2) List image registries used by workloads in this cluster ==="
echo
echo ">> Unique image registries found in all Pods (current namespaces):"
kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.spec.containers[*].image}{" "}{.spec.initContainers[*].image}{"\n"}{end}' \
| tr ' ' '\n' \
| sed '/^$/d' \
| awk -F/ '{print $1}' \
| sort -u
echo
echo ">> ACR registries referenced (ends with .azurecr.io):"
ACR_LIST=$(kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.spec.containers[*].image}{" "}{.spec.initContainers[*].image}{"\n"}{end}' \
| tr ' ' '\n' \
| sed '/^$/d' \
| awk -F/ '{print $1}' \
| grep -Ei '\.azurecr\.io$' \
| sort -u || true)
if [[ -z "${ACR_LIST}" ]]; then
echo "No ACR registries referenced by current workloads."
exit 0
fi
echo "${ACR_LIST}"
echo
echo "=== 3) For each ACR, report cluster-related role assignments ==="
echo
echo "NOTE: You must set AZURE_SUBSCRIPTION_ID, and optionally CLUSTER_PRINCIPAL_ID"
echo " to the kubelet/cluster managed identity (or service principal) Object ID."
echo
if [[ -z "${AZURE_SUBSCRIPTION_ID:-}" ]]; then
echo "ERROR: AZURE_SUBSCRIPTION_ID not set. Export it and re-run:"
echo " export AZURE_SUBSCRIPTION_ID=<subscription-id>"
exit 1
fi
az account set --subscription "${AZURE_SUBSCRIPTION_ID}"
# Optional: identity object ID to filter on (cluster identity / kubelet identity)
CLUSTER_PRINCIPAL_ID="${CLUSTER_PRINCIPAL_ID:-}"
for REGISTRY in ${ACR_LIST}; do
echo
echo "----- ACR: ${REGISTRY} -----"
ACR_NAME="${REGISTRY%%.azurecr.io}"
# Get ACR resource ID
ACR_ID=$(az acr show --name "${ACR_NAME}" --query id -o tsv 2>/dev/null || true)
if [[ -z "${ACR_ID}" ]]; then
echo "Could not resolve ACR ${ACR_NAME} in subscription ${AZURE_SUBSCRIPTION_ID}."
echo "Ensure ACR exists in this subscription or run against the correct subscription."
continue
fi
echo "ACR resource ID: ${ACR_ID}"
echo
echo ">> Role assignments on this ACR (trimmed to possibly cluster-related principals):"
if [[ -n "${CLUSTER_PRINCIPAL_ID}" ]]; then
echo "Filtering to principalId = ${CLUSTER_PRINCIPAL_ID}"
az role assignment list \
--scope "${ACR_ID}" \
--query "[?principalId=='${CLUSTER_PRINCIPAL_ID}'].[roleDefinitionName,principalId,principalType]" \
-o table
else
echo "No CLUSTER_PRINCIPAL_ID provided; showing *all* assignments on this ACR."
echo "You must visually identify the AKS-related identity (kubelet/cluster)."
az role assignment list \
--scope "${ACR_ID}" \
--query "[].{role:roleDefinitionName, principalId:principalId, principalType:principalType}" \
-o table
fi
echo
echo ">> (Optional) Show built-in roles relevant to ACR for reference:"
az role definition list \
--name 'AcrPull' \
--query '[].{name:roleName, id:name, permissions:permissions[0].actions}' \
-o table
az role definition list \
--name 'AcrPush' \
--query '[].{name:roleName, id:name, permissions:permissions[0].actions}' \
-o table
done
echo
echo "=== 4) INTERPRETING THE OUTPUT (what indicates a problem) ==="
cat <<'EOF'
You are checking how the AKS cluster (via its managed identity or service
principal) is allowed to access each ACR.
Focus on role assignments where:
- principalType is 'ServicePrincipal' or 'ManagedIdentity', and
- principalId matches the AKS cluster / kubelet identity.
Problematic cases (NOT read-only):
- The AKS identity has 'AcrPush' or any custom role including actions like:
- Microsoft.ContainerRegistry/registries/push/action
- Microsoft.ContainerRegistry/registries/write
- Microsoft.ContainerRegistry/registries/delete
- The identity has high-privilege roles such as:
- Owner
- Contributor
- AcrDelete
- Any custom role with write/delete privileges on ACR
Acceptable (read-only) access:
- The AKS identity has only 'AcrPull' on the ACR,
or a custom role limited to read/pull operations.
Use this report to:
- Identify which ACRs the cluster pulls from.
- Confirm that the cluster identity is NOT granted push/write/delete
permissions to those registries.
EOF