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
Remediation​
Manual Steps
-
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).
- On any machine with Azure CLI and access to the subscription, list the cluster and its node resource group:
-
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 tsvACR_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).
- List ACRs in the subscription:
-
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.
- For each identity used by AKS:
-
Restrict existing over-privileged role assignments
- For each over-privileged assignment (example:
AcrPush,Contributor,Ownerfor 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
idof the specific assignment to remove, then:az role assignment delete --ids "<ROLE_ASSIGNMENT_ID>" - If the cluster still needs pull access and has no
AcrPullassignment, add it:az role assignment create \--assignee <AKS_PRINCIPAL_ID> \--scope "$ACR_ID" \--role "AcrPull"
- For each over-privileged assignment (example:
-
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.
- Check subscription- or resource-group-level assignments that might indirectly grant write access to ACR:
-
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.
- For each ACR used by the cluster, confirm only
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."