Skip to main content

Minimize Cluster Access To Read-Only For Azure Container

More Info:​

Restrict the AKS clusters access to Azure Container Registry to read-only (pull) so the cluster cannot push or modify images.

Risk Level​

Medium

Address​

Security

Compliance Standards​

  • CIS AKS

Triage and Remediation​

Remediation​

Manual Steps
  1. Identify how AKS pulls images from ACR

    • On any machine with Azure CLI and access to the subscription, list the cluster and its node resource group:
      az aks show \
      --resource-group <AKS_RESOURCE_GROUP> \
      --name <AKS_CLUSTER_NAME> \
      --query "{identity:identity, nodeResourceGroup:nodeResourceGroup}" \
      --output json
    • Determine whether you are using:
      • Managed identity (system-assigned or user-assigned) attached to the AKS cluster, or
      • A service principal, or
      • ACR firewall + private endpoint only (no identity/RBAC link).
  2. Enumerate ACR instances and check role assignments

    • List ACRs in the subscription:
      az acr list --output table
    • For each ACR, list role assignments relevant to AKS (replace <ACR_NAME>):
      az acr show --name <ACR_NAME> --query "id" --output tsv
      ACR_ID=$(az acr show --name <ACR_NAME> --query "id" --output tsv)

      az role assignment list \
      --scope "$ACR_ID" \
      --output table
    • In the output, look for assignments to:
      • The AKS managed identity (object/principal ID from step 1), or
      • The AKS service principal.
    • Note roles such as AcrPull (read-only) vs broader ones (AcrPush, Owner, Contributor, AcrImageSigner, custom roles that include write/delete actions).
  3. Decide required access and plan corrections

    • For each identity used by AKS:
      • If only image pulling is required, the desired role is AcrPull at the minimal required scope (ideally the specific ACR, not subscription or resource group).
      • If broader roles are in use, confirm whether any workload or pipeline actually needs push/delete/sign operations from inside the cluster; if so, limit that to a separate identity not bound to the entire cluster where possible.
  4. Restrict existing over-privileged role assignments

    • For each over-privileged assignment (example: AcrPush, Contributor, Owner for the AKS identity on ACR), remove it:
      az role assignment list \
      --scope "$ACR_ID" \
      --assignee <AKS_PRINCIPAL_ID> \
      --output json
    • From the JSON, copy the id of the specific assignment to remove, then:
      az role assignment delete --ids "<ROLE_ASSIGNMENT_ID>"
    • If the cluster still needs pull access and has no AcrPull assignment, add it:
      az role assignment create \
      --assignee <AKS_PRINCIPAL_ID> \
      --scope "$ACR_ID" \
      --role "AcrPull"
  5. Review other access paths to ACR (optional but recommended)

    • Check subscription- or resource-group-level assignments that might indirectly grant write access to ACR:
      az role assignment list \
      --assignee <AKS_PRINCIPAL_ID> \
      --all \
      --output table
    • If you find higher-scope roles that include ACR write/delete actions and are not strictly required, plan to replace them with more granular, read-only assignments where feasible.
  6. Verify effective read-only configuration

    • For each ACR used by the cluster, confirm only AcrPull (or other strictly read-only custom roles you have vetted) are assigned to the AKS identity at the ACR scope:
      az role assignment list \
      --scope "$ACR_ID" \
      --assignee <AKS_PRINCIPAL_ID> \
      --output table
    • Independently verify that push/delete is no longer allowed from the cluster identity by attempting a test push using that identity’s context from a non-production environment, ensuring it fails while pulls still succeed.
Using kubectl

kubectl cannot change Azure Container Registry permissions or the AKS-to-ACR integration; those settings are managed in Azure (portal/CLI/IaC) at the cloud provider/control plane level. Refer to the Manual Steps section for how to review and adjust AKS access to ACR to be read-only (pull).

Automation
#!/usr/bin/env bash
set -euo pipefail

# PURPOSE:
# - Enumerate all AKS node identities and cluster-managed identities (if any)
# - Enumerate all role assignments these identities have against Azure Container Registries (ACR)
# - Highlight any roles that can push/modify images (problematic) vs pull-only (expected)
#
# REQUIREMENTS:
# - Azure CLI logged in: az login
# - Subscription set: az account set --subscription "<SUBSCRIPTION_ID>"
# - kubectl configured for the AKS cluster
#
# RUN ON:
# - Any machine with Azure CLI and kubectl access to the AKS cluster

echo "=== 1. Discover AKS cluster resource group and node resource group ==="
# Try to infer resource groups from current context
CURRENT_CONTEXT="$(kubectl config current-context)"
echo "Current kube context: ${CURRENT_CONTEXT}"

# List AKS clusters and prompt user to confirm the relevant one
echo
echo "Available AKS clusters in current subscription (for reference):"
az aks list --query '[].{name:name, resourceGroup:resourceGroup}' -o table || true
echo

read -r -p "Enter AKS cluster name: " AKS_NAME
read -r -p "Enter AKS cluster resource group: " AKS_RG

echo
echo "Fetching node resource group for AKS cluster..."
NODE_RG="$(az aks show -n "${AKS_NAME}" -g "${AKS_RG}" --query nodeResourceGroup -o tsv)"
echo "AKS cluster resource group: ${AKS_RG}"
echo "AKS node resource group: ${NODE_RG}"
echo

echo "=== 2. Identify AKS node identities ==="
# For user-assigned identities or VMSS identities backing nodes
echo "Listing VM Scale Sets in node resource group (for node identities)..."
az vmss list -g "${NODE_RG}" --query '[].{name:name, identity:identity}' -o json | jq '.[]' || true
echo
echo "NOTE:"
echo "- For each VMSS, the 'identity' block contains the principalId / userAssignedIdentities."
echo "- Copy the principalId values below if you want to manually inspect them:"
az vmss list -g "${NODE_RG}" --query '[].{name:name, principalIds:identity.principalId}' -o table || true
echo

echo "=== 3. Identify ACR registries and their scopes ==="
echo "Listing all Azure Container Registries in the subscription..."
az acr list --query '[].{name:name, resourceGroup:resourceGroup, loginServer:loginServer, id:id}' -o table
echo
read -r -p "Optionally, restrict analysis to a single ACR name (leave blank for all): " ACR_FILTER

if [[ -n "${ACR_FILTER}" ]]; then
echo "Filtering to ACR: ${ACR_FILTER}"
ACR_IDS=($(az acr list --query "[?name=='${ACR_FILTER}'].id" -o tsv))
else
ACR_IDS=($(az acr list --query '[].id' -o tsv))
fi

if [[ "${#ACR_IDS[@]}" -eq 0 ]]; then
echo "No ACR registries found in this subscription. Exiting."
exit 0
fi

echo "=== 4. Gather role definitions that can modify images ==="
# Roles that typically allow PUSH or modify capabilities to ACR
# (This list is not exhaustive; adjust per your org's custom roles.)
PUSH_CAPABLE_ROLES=(
"AcrPush"
"AcrDelete"
"AcrImageSigner"
"Owner"
"Contributor"
)

echo "Roles considered problematic (write/push or broad rights) for ACR access:"
printf ' - %s\n' "${PUSH_CAPABLE_ROLES[@]}"
echo

echo "=== 5. Enumerate role assignments on each ACR ==="
PROBLEM_FOUND=0

for ACR_ID in "${ACR_IDS[@]}"; do
echo
echo ">>> Checking ACR: ${ACR_ID}"
echo "Role assignments on this ACR (scope = registry only, not parents):"
az role assignment list \
--scope "${ACR_ID}" \
--include-inherited false \
-o json > /tmp/acr_role_assignments.json

# Pretty-print all assignments first for review
jq '.[] | {principalId, principalType, roleDefinitionName, scope}' /tmp/acr_role_assignments.json || true

echo
echo "Scanning for principals with PUSH/MODIFY-capable roles..."
jq -r '.[] | "\(.principalId)\t\(.principalType)\t\(.roleDefinitionName)\t\(.scope)"' /tmp/acr_role_assignments.json \
| while IFS=$'\t' read -r PRINCIPAL_ID PRINCIPAL_TYPE ROLE_NAME SCOPE; do
for BAD_ROLE in "${PUSH_CAPABLE_ROLES[@]}"; do
if [[ "${ROLE_NAME}" == "${BAD_ROLE}" ]]; then
PROBLEM_FOUND=1
echo "!!! PROBLEMATIC ROLE ASSIGNMENT DETECTED:"
echo " PrincipalId: ${PRINCIPAL_ID}"
echo " PrincipalType: ${PRINCIPAL_TYPE}"
echo " RoleDefinitionName: ${ROLE_NAME}"
echo " Scope: ${SCOPE}"
echo " -> This principal can likely PUSH or MODIFY images in this ACR."
echo
fi
done
done
done

echo "=== 6. Cross-check: likely identities used by the AKS cluster ==="
echo
echo "Cluster managed identity / service principal:"
az aks show -n "${AKS_NAME}" -g "${AKS_RG}" \
--query '{identity: identity, servicePrincipalProfile: servicePrincipalProfile}' -o json | jq '.'

echo
echo "Compare the principalId(s) above against the problematic role assignments listed earlier."

echo
echo "=== 7. Interpretation of results ==="
if [[ "${PROBLEM_FOUND}" -eq 0 ]]; then
echo "No PUSH/MODIFY-capable ACR roles were found at registry scope for any principal."
echo "From a registry-scope perspective, cluster access is likely limited to pull-only (or not granted)."
else
echo "One or more principals have ACR roles that allow image push/delete or broad modification."
echo
echo "WHAT OUTPUT INDICATES A PROBLEM:"
echo "- Any '!!! PROBLEMATIC ROLE ASSIGNMENT DETECTED' block for a principal that is:"
echo " * The AKS cluster managed identity/service principal, or"
echo " * A VMSS / node managed identity used by the cluster."
echo
echo "These indicate the cluster (or its nodes) has more than read-only (pull) access to ACR."
echo
echo "This check is MANUAL: review whether those principals NEED push/modify rights."
echo "If not, adjust role assignments via Azure Portal / CLI / IaC to use pull-only roles (e.g. AcrPull)."
fi

echo
echo "=== 8. Optional: verify cluster identities explicitly against ACR scopes ==="
echo "You can explicitly look up role assignments for a specific principalId and ACR:"
echo "Example (replace values):"
echo " PRINCIPAL_ID=\"<principal-guid>\""
echo " ACR_ID=\"<acr-resource-id>\""
echo " az role assignment list --assignee \"\$PRINCIPAL_ID\" --scope \"\$ACR_ID\" -o table"
echo
echo "End of report."