Skip to main content

Minimize User Access To Azure Container Registry

More Info:​

Grant only the minimum permissions required for the AKS cluster service principal to read and pull images from Azure Container Registry, avoiding broad Owner or account administrator roles.

Risk Level​

Medium

Address​

Security

Compliance Standards​

  • CIS AKS

Triage and Remediation​

Remediation​

Manual Steps
  1. Identify the AKS cluster’s identity and attached ACRs

    • Run on any machine with Azure CLI access:
      # Get cluster identity info
      az aks show \
      --resource-group <AKS_RESOURCE_GROUP> \
      --name <AKS_CLUSTER_NAME> \
      --query "{type:identity.type, principalId:identity.principalId, userAssignedIdentities:identity.userAssignedIdentities}" \
      --output json

      # List all ACRs in the subscription (or restrict by resource group)
      az acr list --output table
    • Determine whether the cluster uses a system-assigned identity, user-assigned managed identity, or legacy service principal.
  2. List current role assignments on each ACR and identify over-privileged roles

    • For each registry (replace with actual names and resource groups):
      ACR_NAME=<ACR_NAME>
      ACR_RG=<ACR_RESOURCE_GROUP>

      # Get the ACR resource ID
      ACR_ID=$(az acr show --name "$ACR_NAME" --resource-group "$ACR_RG" --query id -o tsv)

      # List role assignments scoped to this ACR
      az role assignment list \
      --scope "$ACR_ID" \
      --output table
    • Look for assignments of broad roles like Owner, Contributor, or subscription/resource-group–scoped roles to the AKS identity or to generic user/service principals that are not required for image pull.
  3. Verify that the AKS identity has only the minimum ACR role (AcrPull or AcrPull-equivalent)

    • From the output above, for the AKS identity principalId (or service principal appId), confirm that:
      • Scope is the ACR resource (not entire subscription or resource group) unless broader scope is intentionally required.
      • Role is AcrPull (or AcrImageSigner/AcrPush only where there is a clear business need).
    • If AKS authenticates using image pull secrets instead of managed identity, identify the underlying principal:
      # On any machine with kubectl access
      kubectl get secret --all-namespaces | grep dockerconfigjson || true
    • Review who manages the credentials used in those secrets and what roles they have on the ACR (via az role assignment list as above).
  4. Remove over-privileged role assignments and reassign least-privilege roles

    • For any AKS identity or service principal that has Owner, Contributor, or other broad roles on the ACR and does not require them, remove the assignment:
      # Example: remove an over-privileged role assignment by ID
      az role assignment list \
      --scope "$ACR_ID" \
      --assignee <PRINCIPAL_ID_OR_APPID> \
      --output table

      # Note the 'Id' field from the row you want to remove, then:
      az role assignment delete --ids <ROLE_ASSIGNMENT_ID>
    • Then assign the minimal required role (AcrPull) scoped to the ACR:
      az role assignment create \
      --assignee <PRINCIPAL_ID_OR_APPID> \
      --role "AcrPull" \
      --scope "$ACR_ID"
    • If needed, configure or update the AKS–ACR integration so AKS uses this principal:
      # Using system-assigned identity
      az aks update \
      --resource-group <AKS_RESOURCE_GROUP> \
      --name <AKS_CLUSTER_NAME> \
      --attach-acr "$ACR_NAME"
      # Or, if using a specific service principal/managed identity, configure per your chosen auth pattern.
  5. Optionally migrate from legacy service principal / broad roles to dedicated identity for ACR access

    • If the AKS cluster or image-pulling process currently uses a generic Owner/Contributor principal, create or select a dedicated identity with only ACR permissions:
      # Example: create a user-assigned managed identity
      az identity create \
      --name <ACR_PULL_IDENTITY_NAME> \
      --resource-group <IDENTITY_RESOURCE_GROUP>

      # Grant AcrPull on the ACR to this identity
      PULL_MI_PRINCIPAL_ID=$(az identity show \
      --name <ACR_PULL_IDENTITY_NAME> \
      --resource-group <IDENTITY_RESOURCE_GROUP> \
      --query principalId -o tsv)

      az role assignment create \
      --assignee "$PULL_MI_PRINCIPAL_ID" \
      --role "AcrPull" \
      --scope "$ACR_ID"
    • Integrate this identity into AKS either as the cluster managed identity or via pull secrets, following your chosen authentication pattern.
  6. Verify least-privilege configuration and cluster image pull functionality

    • Re-list ACR role assignments and confirm only minimal roles remain for AKS-related principals:
      az role assignment list \
      --scope "$ACR_ID" \
      --output table
    • Validate that pods can still pull images from ACR:
      # On any machine with kubectl access
      kubectl run acr-pull-test \
      --image <ACR_NAME>.azurecr.io/<REPO>/<IMAGE>:<TAG> \
      --restart=Never --rm -it
    • If the test pod starts successfully and the role assignments show only scoped AcrPull (and any explicitly justified roles), the configuration meets the intent of the control.
Using kubectl

kubectl cannot change Azure Container Registry role assignments or AKS–ACR integration, because these live in Azure RBAC and AKS configuration managed via the Azure portal, Azure CLI, or IaC. To address this finding, make the changes described in the Manual Steps section using those cloud provider tools.

Automation
#!/usr/bin/env bash
#
# Audit AKS -> ACR access for excessive permissions
# Requirements:
# - Azure CLI logged in with rights to read AKS, ACR and role assignments
# - jq installed
#
# Run on: any machine with az, jq, and access to the subscription(s)

set -euo pipefail

# -----------------------------
# CONFIGURATION
# -----------------------------
# Space-separated list of subscriptions to inspect
SUBSCRIPTIONS="SUBSCRIPTION_ID_1 SUBSCRIPTION_ID_2"

echo "=== AKS to ACR access review ==="
date
echo

for SUB in $SUBSCRIPTIONS; do
echo "=== Subscription: $SUB ==="
az account set -s "$SUB"

# List AKS clusters
CLUSTERS_JSON=$(az aks list -o json)
if [[ "$(echo "$CLUSTERS_JSON" | jq 'length')" -eq 0 ]]; then
echo "No AKS clusters found in $SUB"
echo
continue
fi

echo "$CLUSTERS_JSON" | jq -r '.[] | "\(.name) \(.resourceGroup)"' | while read -r AKS_NAME RG; do
echo "---- AKS cluster: $AKS_NAME (RG: $RG) ----"

# Try to get the identity used by the cluster for ACR pulls
# 1) user-assigned or system-assigned managed identity
IDENTITY_TYPE=$(echo "$CLUSTERS_JSON" | jq -r ".[] | select(.name==\"$AKS_NAME\" and .resourceGroup==\"$RG\") | .identity.type // \"\"")
ID_PRINCIPAL_ID=""

if [[ "$IDENTITY_TYPE" == "SystemAssigned" || "$IDENTITY_TYPE" == "SystemAssigned, UserAssigned" || "$IDENTITY_TYPE" == "UserAssigned" ]]; then
# Primary identity principalId
ID_PRINCIPAL_ID=$(echo "$CLUSTERS_JSON" | jq -r ".[] | select(.name==\"$AKS_NAME\" and .resourceGroup==\"$RG\") | .identity.principalId // \"\"")
if [[ -n "$ID_PRINCIPAL_ID" && "$ID_PRINCIPAL_ID" != "null" ]]; then
echo " Managed identity principalId: $ID_PRINCIPAL_ID"
fi
fi

# 2) Legacy service principal (if present)
SP_CLIENT_ID=$(az aks show -n "$AKS_NAME" -g "$RG" --query "servicePrincipalProfile.clientId" -o tsv 2>/dev/null || echo "")
if [[ -n "$SP_CLIENT_ID" && "$SP_CLIENT_ID" != "msi" && "$SP_CLIENT_ID" != "null" ]]; then
echo " Service principal clientId: $SP_CLIENT_ID"
# Resolve to objectId
SP_OBJECT_ID=$(az ad sp list --filter "appId eq '$SP_CLIENT_ID'" --query "[0].id" -o tsv 2>/dev/null || echo "")
if [[ -n "$SP_OBJECT_ID" ]]; then
echo " Service principal objectId: $SP_OBJECT_ID"
fi
fi

# Gather principal IDs to check
PRINCIPALS=()
if [[ -n "${ID_PRINCIPAL_ID:-}" && "$ID_PRINCIPAL_ID" != "null" ]]; then
PRINCIPALS+=("$ID_PRINCIPAL_ID")
fi
if [[ -n "${SP_OBJECT_ID:-}" ]]; then
PRINCIPALS+=("$SP_OBJECT_ID")
fi

if [[ "${#PRINCIPALS[@]}" -eq 0 ]]; then
echo " WARNING: No identifiable principal (managed identity or service principal) found for this cluster."
echo
continue
fi

# 3) Discover linked ACRs via 'az aks show' (automatic linkage)
ACR_IDS=$(az aks show -n "$AKS_NAME" -g "$RG" --query "addonProfiles.aciConnectorLinux.config.subnetName" -o tsv 2>/dev/null || true)
# Fallback: use 'az aks show' | grep acr (for classic ACR integration)
ACR_IDS_FROM_CMD=$(az aks show -n "$AKS_NAME" -g "$RG" --query "servicePrincipalProfile.clientId" -o tsv >/dev/null 2>&1 || true)

# Instead, rely on all ACRs in subscription and later focus on roles they have with these principals
echo " Listing ACRs in subscription..."
ACRS_JSON=$(az acr list -o json)
if [[ "$(echo "$ACRS_JSON" | jq 'length')" -eq 0 ]]; then
echo " No ACR registries found in subscription."
echo
continue
fi

echo " Checking role assignments on ACRs for AKS principals..."
echo

echo "$ACRS_JSON" | jq -r '.[] | "\(.name) \(.id)"' | while read -r ACR_NAME ACR_ID; do
echo " ACR: $ACR_NAME"
for PID in "${PRINCIPALS[@]}"; do
echo " Principal: $PID"
# Role assignments scoped at the registry
RA_JSON=$(az role assignment list --assignee "$PID" --scope "$ACR_ID" -o json 2>/dev/null || echo "[]")

if [[ "$(echo "$RA_JSON" | jq 'length')" -eq 0 ]]; then
echo " No direct role assignments on this ACR."
else
echo " Role assignments on this ACR:"
echo "$RA_JSON" | jq -r '.[] | " Role: \(.roleDefinitionName) | Scope: \(.scope)"'
fi
done
echo
done

# 4) Check if principals have broad roles (Owner/Contributor) at subscription or resource group scope that could implicitly grant ACR access
echo " Checking for broad roles (Owner/Contributor) at subscription and RG scopes..."
for PID in "${PRINCIPALS[@]}"; do
echo " Principal: $PID"

echo " Subscription-scope assignments:"
az role assignment list --assignee "$PID" --scope "/subscriptions/$SUB" \
--query "[?roleDefinitionName=='Owner' || roleDefinitionName=='Contributor'].[roleDefinitionName, scope]" \
-o tsv 2>/dev/null || echo " none"

echo " Resource-group-scope assignments (RG: $RG):"
az role assignment list --assignee "$PID" --scope "/subscriptions/$SUB/resourceGroups/$RG" \
--query "[?roleDefinitionName=='Owner' || roleDefinitionName=='Contributor'].[roleDefinitionName, scope]" \
-o tsv 2>/dev/null || echo " none"
done

echo
done

echo
done

cat <<'EOF'

INTERPRETING RESULTS
--------------------
Potential PROBLEMS (needs review/minimization) include:
1. On any ACR:
- AKS identity/service principal has roles like:
- "Owner"
- "Contributor"
- Any custom role with write/delete/admin actions
Instead, prefer "AcrPull" (or at most "AcrPush" if justified).

2. At subscription or resource group scope:
- AKS identity/service principal is assigned:
- "Owner"
- "Contributor"
This is broader than needed for pulling images.

3. No identifiable principal:
- If the script reports "No identifiable principal" for a cluster, verify how it authenticates
to ACR (managed identity, service principal, or image pull secrets) and ensure access is
not via over-privileged user accounts.

Use this report to manually:
- Remove broad Owner/Contributor assignments where not strictly required.
- Assign least-privilege roles (e.g., AcrPull on the specific ACRs used by the cluster).
- Consider using a dedicated managed identity/service principal per cluster or workload.
EOF