Skip to main content

Prefer Using Dedicated AKS Service Accounts

More Info:​

Use dedicated service accounts integrated with Azure Active Directory so users and groups get scoped, auditable access to Kubernetes resources instead of shared or default identities.

Risk Level​

Medium

Address​

Security

Compliance Standards​

  • CIS AKS

Triage and Remediation​

Remediation​

Manual Steps
  1. Determine whether the AKS cluster is Azure AD–integrated

    • Run on any machine with Azure CLI access:
      az aks show \
      --resource-group <RESOURCE_GROUP_NAME> \
      --name <CLUSTER_NAME> \
      --query "{aadProfile:aadProfile, oidcIssuerProfile:oidcIssuerProfile}" \
      --output json
    • Review the output: confirm that either a legacy aadProfile is configured or oidcIssuerProfile.enabled is true. If neither is present, plan to enable Azure AD / OIDC integration as per your organization’s standards before proceeding.
  2. Inventory who is accessing the cluster and how (AAD users/groups vs. shared identities)

    • List Azure AD app registrations and service principals used for AKS access (run from any machine with Azure AD Graph/MS Graph permissions):
      az ad sp list --filter "servicePrincipalType eq 'Application'" --query "[].{appId:appId, displayName:displayName}" --all -o table
    • Correlate these identities with:
      • The AKS cluster’s managed identity or service principals (identity, servicePrincipalProfile fields from az aks show).
      • Any identities configured in your IaC (ARM/Bicep/Terraform) for cluster access.
    • Decide which of these identities represent shared/broadly used accounts that should not have direct, unscoped access.
  3. Review RBAC bindings to ensure use of scoped service accounts and AAD groups, not defaults/shared identities

    • Get current role bindings and cluster role bindings (any machine with kubectl access):
      kubectl get clusterrolebindings -o wide
      kubectl get rolebindings --all-namespaces -o wide
    • For more detail on subjects:
      kubectl get clusterrolebindings -o yaml
      kubectl get rolebindings --all-namespaces -o yaml
    • Manually inspect subjects and look for:
      • system:anonymous, system:unauthenticated, or system:masters.
      • system:serviceaccount:default:default or other default service accounts used for application workloads.
      • Broad Azure AD groups or generic service principals instead of dedicated, least-privilege identities.
    • Decide which bindings should be refactored to:
      • Dedicated Kubernetes service accounts per application or component.
      • Azure AD groups mapped to appropriate ClusterRoles/Roles for human users.
  4. Check whether workloads use dedicated Kubernetes service accounts vs. defaults

    • List service accounts by namespace (any machine with kubectl access):
      kubectl get sa --all-namespaces
    • Inspect pods to see which service accounts they use:
      kubectl get pods --all-namespaces -o wide
      kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\n"}{end}'
    • Identify applications using default or other shared service accounts instead of dedicated ones. Decide which namespaces/workloads require creation and use of dedicated service accounts and corresponding Role/RoleBindings.
  5. Define and implement the desired model for dedicated AKS service accounts and AAD integration (via console/CLI/IaC)

    • Using Azure Portal, Azure CLI, or your IaC:
      • Ensure the cluster is configured to use Azure AD (managed AAD/OIDC) per step 1 if your policy requires it.
      • Create or update Azure AD groups to represent roles (e.g., aks-dev-readonly, aks-ops-admin) and assign the appropriate users.
      • In your Kubernetes manifests/IaC, define:
        • Dedicated ServiceAccount objects per application or CI/CD component.
        • Role/ClusterRole and RoleBinding/ClusterRoleBinding objects that bind those service accounts and AAD groups with least-privilege access.
    • This step is done through Azure Portal/CLI/IaC authoring; there is no single command that can generically “fix” the cluster because choices are organization- and workload-specific.
  6. Verify that access is now via dedicated service accounts and AAD identities, and not via shared/default identities

    • Re-run:
      kubectl get clusterrolebindings -o yaml
      kubectl get rolebindings --all-namespaces -o yaml
      kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\n"}{end}'
    • Confirm that:
      • Human access is mediated through Azure AD users/groups mapped to appropriate RBAC roles.
      • Application workloads use named, dedicated service accounts instead of default or shared accounts.
      • Any previously identified broad/shared or default identities have been removed or re-scoped according to your access model.
Using kubectl

kubectl can’t configure Azure AD integration or how identities are mapped to AKS; those settings are managed in Azure (portal/CLI/IaC) at the managed control plane level. To address this finding, use Azure AD and AKS configuration as described in the Manual Steps section rather than attempting changes with kubectl.

Automation
#!/usr/bin/env bash
# Report AKS service account usage and AAD integration status
# Run on: any machine with Azure CLI logged-in and kubectl context for the AKS cluster

set -euo pipefail

echo "=== 1. Cluster-level AAD integration status (managed control plane) ==="
# Adjust --resource-group and --name as needed before running
RG="MY-RESOURCE-GROUP"
CLUSTER="MY-AKS-CLUSTER"

echo "# az aks show --resource-group \"$RG\" --name \"$CLUSTER\" --query '{aadProfile:aadProfile,oidcIssuerProfile:oidcIssuerProfile,azureRbac:azurePortalFqdn}' -o json"
az aks show \
--resource-group "$RG" \
--name "$CLUSTER" \
--query '{aadProfile:aadProfile,oidcIssuerProfile:oidcIssuerProfile,azureRbac:azureRbacEnabled}' \
-o json

cat <<'EOF'

Interpretation (AAD / OIDC):
- aadProfile == null or disabled => PROBLEM: cluster not integrated with Azure AD.
- oidcIssuerProfile.enabled == false or null => PROBLEM: OIDC issuer not enabled (limits AAD-backed service account patterns).
- azureRbacEnabled == false => REVIEW: using native Kubernetes RBAC only (not Azure RBAC for Kubernetes).

EOF

echo "=== 2. List all service accounts and their usage across namespaces ==="
echo "# Service accounts (namespaced) with basic metadata"
kubectl get sa --all-namespaces -o custom-columns=\
'NAMESPACE:.metadata.namespace,NAME:.metadata.name,SECRETS:.secrets[*].name' | sort

cat <<'EOF'

Interpretation (service accounts):
- Heavy use of 'default' service account in many namespaces => PROBLEM: shared identity instead of dedicated SAs.
- Few or no non-default SAs in app namespaces => PROBLEM: likely not using dedicated SAs per workload.
EOF

echo "=== 3. Pods using the 'default' service account (by namespace) ==="
kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{";"}{.metadata.name}{";"}{.spec.serviceAccountName}{"\n"}{end}' \
| awk -F';' 'NR==1 || $3=="default"' \
| sort \
| awk -F';' 'BEGIN{printf "%-25s %-40s %-25s\n","NAMESPACE","POD","SERVICEACCOUNT"} {printf "%-25s %-40s %-25s\n",$1,$2,$3}'

cat <<'EOF'

Interpretation:
- Any application pod (non-system namespace) with SERVICEACCOUNT 'default' => PROBLEM:
the pod is not bound to a dedicated, least-privilege service account.

EOF

echo "=== 4. Pods per service account (identify shared SAs) ==="
kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{";"}{.metadata.name}{";"}{.spec.serviceAccountName}{"\n"}{end}' \
| sort \
| awk -F';' '{
key=$1"/"$3;
pods[key]=pods[key] ? pods[key]","$2 : $2;
count[key]++;
}
END{
printf "%-35s %-8s %s\n","NAMESPACE/SA","POD_CNT","PODS";
for (k in pods) {
printf "%-35s %-8d %s\n",k,count[k],pods[k];
}
}' | sort

cat <<'EOF'

Interpretation:
- Service accounts shared by many unrelated applications within a namespace => REVIEW:
may violate "dedicated per workload" intent; investigate if sharing is justified.

EOF

echo "=== 5. Service accounts annotated for projected tokens / OIDC (if any) ==="
kubectl get sa --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{";"}{.metadata.name}{";"}{.metadata.annotations}{"\n"}{end}' \
| grep -E 'azure.workload.identity|kubernetes.io/service-account\.issuer|azure\.com' || true

cat <<'EOF'

Interpretation:
- Absence of annotations related to Azure Workload Identity / OIDC in app namespaces may
indicate limited adoption of AAD-backed service account patterns (not a hard failure,
but a gap if your target architecture expects Azure Workload Identity).

EOF

echo "=== 6. RBAC bindings that reference service accounts ==="
echo "# ClusterRoleBindings with ServiceAccount subjects"
kubectl get clusterrolebindings.rbac.authorization.k8s.io -o json \
| jq -r '.items[]
| select(.subjects != null)
| . as $crb
| .subjects[]
| select(.kind=="ServiceAccount")
| "\($crb.metadata.name);\(.namespace)//\(.name)"' 2>/dev/null \
| awk -F';' 'BEGIN{printf "%-45s %-40s\n","CLUSTERROLEBINDING","SERVICEACCOUNT (ns/name)"} {printf "%-45s %-40s\n",$1,$2}' \
| sort || true

echo
echo "# RoleBindings with ServiceAccount subjects (namespaced)"
kubectl get rolebindings.rbac.authorization.k8s.io --all-namespaces -o json \
| jq -r '.items[]
| select(.subjects != null)
| . as $rb
| .subjects[]
| select(.kind=="ServiceAccount")
| "\($rb.metadata.namespace);\($rb.metadata.name);\(.namespace)//\(.name)"' 2>/dev/null \
| awk -F';' 'BEGIN{printf "%-20s %-40s %-40s\n","NAMESPACE","ROLEBINDING","SERVICEACCOUNT (ns/name)"} {printf "%-20s %-40s %-40s\n",$1,$2,$3}' \
| sort || true

cat <<'EOF'

Interpretation (RBAC):
- Bindings that target 'default' service account in app namespaces with broad roles
(e.g., cluster-admin, edit) => PROBLEM: shared, over-privileged identity.
- Service accounts without any bindings might be unused (REVIEW: remove or repurpose).

EOF

echo "=== 7. Namespaces and default service account token mounting policy ==="
kubectl get ns -o jsonpath='{range .items[*]}{.metadata.name}{";"}{.metadata.annotations.pod-security\.kubernetes\.io/enforce}{";"}{.metadata.annotations."kubernetes.io/enforce-mount-token"}{"\n"}{end}' 2>/dev/null \
| awk -F';' 'BEGIN{printf "%-25s %-15s %-25s\n","NAMESPACE","POD-SECURITY","ENFORCE-MOUNT-TOKEN"} {printf "%-25s %-15s %-25s\n",$1,$2,$3}' \
| sort || true

cat <<'EOF'

Interpretation:
- Namespaces where pods auto-mount tokens for 'default' SA and no explicit dedicated SAs
are used => PROBLEM: broad, implicit access surface.

EOF

echo "=== REVIEW SUMMARY (what indicates a problem) ==="
cat <<'EOF'
Problem indicators to flag for manual review:
1) Cluster not AAD-integrated or OIDC issuer disabled:
- az aks show reports:
- aadProfile == null or enableAzureRBAC/aadProfile.enabled == false
- oidcIssuerProfile.enabled == false or null

2) Workloads using shared or default identities:
- Many pods (especially in app namespaces) show SERVICEACCOUNT 'default'.
- Few or no non-default service accounts in those namespaces.
- Single service account used by many different apps without clear justification.

3) Overbroad RBAC on shared accounts:
- RoleBinding / ClusterRoleBinding granting strong roles (admin, edit, cluster-admin)
to:
- default service account, or
- service accounts used by multiple applications.

4) Lack of AAD-backed patterns where required:
- Organization standard requires Azure AD / Workload Identity, but:
- Cluster AAD/OIDC disabled, and
- No Azure Workload Identity / OIDC-related annotations on app service accounts.

These findings do NOT auto-fix the issue; they provide evidence for a human review to
decide on:
- Enabling / tightening AAD and OIDC integration at the AKS control plane.
- Creating dedicated per-workload service accounts and updating pod specs.
- Adjusting RBAC so each service account has least-privilege access.
EOF