Minimize User Access Azure Container Registry
More Info:
Scan images being deployed to Amazon EKS for vulnerabilities.
Risk Level
High
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
-
Identify AKS cluster identity and attached registries
- Run on: any machine with Azure CLI access.
- Commands:
# Get AKS cluster details (replace with your values)az aks show \--resource-group <AKS_RESOURCE_GROUP> \--name <AKS_CLUSTER_NAME> \--query "{identity:identity,servicePrincipalProfile:servicePrincipalProfile,agentPoolProfiles:agentPoolProfiles}" \-o json# List all ACRs in the subscription (or scope to a resource group)az acr list -o table
- Review: Determine whether the cluster uses a managed identity or a service principal and list which ACRs are in scope for the cluster.
-
List current role assignments on each ACR and identify human users
- Run on: any machine with Azure CLI access.
- Commands (per registry):
REGISTRY_NAME=<ACR_NAME># Get the ACR resource IDACR_ID=$(az acr show --name "$REGISTRY_NAME" --query id -o tsv)# List all role assignments scoped to this ACRaz role assignment list \--scope "$ACR_ID" \-o table
- Review:
- Flag any
principalTypeofUserorGroupthat are not strictly required to push/pull images. - Note any assignments with broad roles like
Owner,Contributor, orAcrPush/AcrPullgranted to large groups.
- Flag any
-
Verify AKS cluster’s ACR access is scoped minimally and uses non-owner roles
- Run on: any machine with Azure CLI access.
- Commands:
# Example: list only role assignments for the AKS managed identity/SP on this ACR# First, capture the AKS principalId from step 1 (AKS_PRINCIPAL_ID)AKS_PRINCIPAL_ID=<AKS_PRINCIPAL_ID_FROM_STEP_1>ACR_ID=$(az acr show --name <ACR_NAME> --query id -o tsv)az role assignment list \--assignee "$AKS_PRINCIPAL_ID" \--scope "$ACR_ID" \-o table
- Review:
- Ensure the AKS identity has only
AcrPullon the ACR (and notOwner/Contributor). - Confirm it is not granted at subscription or resource-group scope unless strictly required.
- Ensure the AKS identity has only
-
Remove unnecessary direct user access and replace with least-privilege roles
- Run on: any machine with Azure CLI access (requires appropriate permissions).
- For each unnecessary or overly broad assignment identified in steps 2–3:
# Example: remove a specific role assignmentaz role assignment delete \--assignee <USER_OR_GROUP_OBJECT_ID_OR_UPN> \--scope "$ACR_ID" \--role "<ROLE_NAME>"
- If users still need image access, prefer:
- Assign
AcrPullto a small, well-defined group for read-only access. - Assign
AcrPushonly to CI/CD service principals or automation identities that build images, not to broad user groups.
- Assign
-
Ensure AKS is integrated with ACR using a dedicated identity instead of broad roles
- Run on: any machine with Azure CLI access.
- If AKS is not yet integrated or relies on subscription-level permissions, reconfigure using the recommended pattern:
# Grant the AKS identity AcrPull on the registryACR_ID=$(az acr show --name <ACR_NAME> --query id -o tsv)AKS_PRINCIPAL_ID=<AKS_PRINCIPAL_ID_FROM_STEP_1>az role assignment create \--assignee "$AKS_PRINCIPAL_ID" \--scope "$ACR_ID" \--role "AcrPull"
- Optionally, if you must integrate via
az aks updatefor managed identity wiring:az aks update \--resource-group <AKS_RESOURCE_GROUP> \--name <AKS_CLUSTER_NAME> \--attach-acr <ACR_NAME>
-
Verify reduced ACR access and document exceptions
- Run on: any machine with Azure CLI access.
- Re-run role assignment listing to confirm:
ACR_ID=$(az acr show --name <ACR_NAME> --query id -o tsv)az role assignment list \--scope "$ACR_ID" \-o table
- Verification criteria:
- Only necessary identities (AKS cluster identity, CI/CD service principals, tightly scoped groups) have
AcrPull/AcrPush. - No
Owner/Contributorassignments remain on the ACR except for strictly justified operational accounts, which are documented.
- Only necessary identities (AKS cluster identity, CI/CD service principals, tightly scoped groups) have
Using kubectl
kubectl cannot be used to change Azure Container Registry permissions or AKS–ACR integration; those are managed in Azure (portal, az CLI, or IaC) at the cloud/provider level. To remediate this finding, follow the guidance in the Manual Steps section using Azure tools rather than kubectl.
Automation
#!/usr/bin/env bash
#
# Report ACR access for review (CIS AKS 5.1.2)
#
# Requirements:
# - Azure CLI logged in and set to the correct subscription
# - jq installed
# - kubectl configured for the target AKS cluster
set -euo pipefail
# ---------- CONFIG ----------
SUBSCRIPTION_ID="<SUBSCRIPTION_ID>"
AKS_RG="<AKS_RESOURCE_GROUP>"
AKS_NAME="<AKS_CLUSTER_NAME>"
# ----------------------------
az account set --subscription "$SUBSCRIPTION_ID" >/dev/null
echo "=== AKS CLUSTER IDENTITY ==="
AKS_JSON="$(az aks show -g "$AKS_RG" -n "$AKS_NAME" -o json)"
AKS_IDENTITY_TYPE="$(echo "$AKS_JSON" | jq -r '.identity.type // "sp"')"
if [[ "$AKS_IDENTITY_TYPE" == "SystemAssigned" || "$AKS_IDENTITY_TYPE" == "UserAssigned" || "$AKS_IDENTITY_TYPE" == "SystemAssigned,UserAssigned" ]]; then
# Managed identity
NODE_MI_PRINCIPAL_ID="$(echo "$AKS_JSON" | jq -r '.identityProfile.kubeletidentity.clientId // empty')"
CLUSTER_MI_PRINCIPAL_ID="$(echo "$AKS_JSON" | jq -r '.identity.principalId // empty')"
echo "AKS uses managed identity."
echo " Kubelet identity clientId: ${NODE_MI_PRINCIPAL_ID:-<none>}"
echo " Cluster identity principalId: ${CLUSTER_MI_PRINCIPAL_ID:-<none>}"
else
# Service principal
SP_CLIENT_ID="$(echo "$AKS_JSON" | jq -r '.servicePrincipalProfile.clientId // empty')"
echo "AKS uses service principal."
echo " Service principal clientId: ${SP_CLIENT_ID:-<none>}"
fi
echo
echo "=== DISCOVER ACR REGISTRIES IN SUBSCRIPTION ==="
ACR_LIST_JSON="$(az acr list -o json)"
if [[ "$(echo "$ACR_LIST_JSON" | jq 'length')" -eq 0 ]]; then
echo "No ACR registries found in subscription."
exit 0
fi
echo "$ACR_LIST_JSON" | jq -r '.[] | "\(.name)\t\(.loginServer)\t\(.id)"' | \
awk 'BEGIN{OFS="\t"; print "ACR_NAME","LOGIN_SERVER","ACR_ID"}1'
echo
echo "=== ACR ROLE ASSIGNMENTS (FOCUS ON USER IDENTITIES AND AKS IDENTITIES) ==="
echo "NOTE: Review for:"
echo " - Direct user principals (userPrincipalName/email) with push/pull access"
echo " - Broad roles: Owner, Contributor, AcrPush, AcrPull, AcrDelete on ACR scope"
echo " - Group assignments that include many users"
echo
while IFS=$'\t' read -r ACR_NAME LOGIN_SERVER ACR_ID; do
[[ "$ACR_NAME" == "ACR_NAME" ]] && continue
echo "---- ACR: $ACR_NAME ($LOGIN_SERVER) ----"
echo "Scope: $ACR_ID"
echo
# List all role assignments scoped at this ACR (and below)
RA_JSON="$(az role assignment list --scope "$ACR_ID" -o json)"
if [[ "$(echo "$RA_JSON" | jq 'length')" -eq 0 ]]; then
echo " No role assignments scoped directly to this ACR."
echo
continue
fi
echo " Role assignments:"
echo " (principalType: user, group, ServicePrincipal, MSI, etc.)"
echo " (flag '*' in SUSPICIOUS if broad roles or user/group assignments)"
echo
echo "principalType\tprincipalName/principalId\troleDefinitionName\tSUSPICIOUS" | expand -t 20
echo "$RA_JSON" | jq -r '
.[] |
.principalType as $pt |
.roleDefinitionName as $role |
.principalId as $pid |
. | {
principalType: $pt,
roleDefinitionName: $role,
principalId: $pid
} | @tsv' | \
while IFS=$'\t' read -r PT ROLE PID; do
SUS=""
# Identify broad roles on this ACR
case "$ROLE" in
"Owner"|"Contributor"|"AcrPush"|"AcrPull"|"AcrDelete")
SUS="*"
;;
esac
# Principal name resolution (may fail; fallback to ID)
PRINCIPAL_NAME="$PID"
if [[ "$PT" == "User" || "$PT" == "Group" || "$PT" == "ServicePrincipal" ]]; then
# Try to resolve display name / UPN
NAME_JSON="$(az ad object show --id "$PID" -o json 2>/dev/null || true)"
if [[ -n "$NAME_JSON" ]]; then
DISPLAY_NAME="$(echo "$NAME_JSON" | jq -r '.displayName // empty')"
UPN="$(echo "$NAME_JSON" | jq -r '.userPrincipalName // empty')"
if [[ -n "$UPN" ]]; then
PRINCIPAL_NAME="$UPN"
elif [[ -n "$DISPLAY_NAME" ]]; then
PRINCIPAL_NAME="$DISPLAY_NAME"
fi
fi
# Any user or group with ACR roles should be reviewed
if [[ "$PT" == "User" || "$PT" == "Group" ]]; then
SUS="*"
fi
fi
printf "%s\t%s\t%s\t%s\n" "$PT" "$PRINCIPAL_NAME" "$ROLE" "$SUS" | expand -t 20
done
echo
done
echo "=== KUBERNETES IMAGE SOURCES (FOR CONTEXT) ==="
echo "Collected from current workloads. This does NOT prove who can access ACR; it shows what registries are in use."
kubectl get pods --all-namespaces -o json | \
jq -r '
.items[] |
.metadata.namespace as $ns |
.metadata.name as $pod |
.spec.containers[]?.image as $img |
[$ns, $pod, $img] | @tsv' | \
awk 'BEGIN{OFS="\t"; print "NAMESPACE","POD","IMAGE"}1' | \
sed 's/\t/ /g'
Explanation of output indicating a potential problem (to review manually):
- Any line under an ACR with:
principalTypeofUserorGroupand any ACR-related role (AcrPull,AcrPush,AcrDelete,Contributor,Owner) is suspicious (SUSPICIOUScolumn shows*).- Very broad roles (
Owner,Contributor) assigned at ACR scope to any principal are suspicious.
- For AKS, preferred pattern is:
- Only the AKS kubelet identity / service principal (and possibly tightly scoped automation identities) have
AcrPull(andAcrPushonly where necessary).
- Only the AKS kubelet identity / service principal (and possibly tightly scoped automation identities) have
- The list of Kubernetes images helps you confirm which ACRs (login servers) are actually in use and whether any unused ACRs still grant user access.