Skip to main content

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

Manual Steps
  1. 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 table

    Identify which registries are referenced in your Kubernetes image references (e.g., myregistry.azurecr.io/...).

  2. 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 identities
    az 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 json

    Then list role assignments on each ACR:

    az role assignment list \
    --scope $(az acr show --name ACR_NAME --query id -o tsv) \
    --output json
  3. Review the role assignments from step 2 and determine whether any AKS identities have write-capable roles on the ACR, such as:

    • AcrPush
    • Owner
    • Contributor
    • Any custom role that includes Microsoft.ContainerRegistry/registries/push/*, write, or delete actions.
      Keep read-only roles (e.g., AcrPull or custom roles limited to pull/list) and flag any broader roles for correction.
  4. 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)
  5. 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 json

    Adjust roles so only non-cluster identities used for image publishing retain push/write permissions.

  6. 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 table

    Confirm 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

Additional Reading: