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
Remediation
Manual Steps
-
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
aadProfileis configured oroidcIssuerProfile.enabledistrue. If neither is present, plan to enable Azure AD / OIDC integration as per your organization’s standards before proceeding.
- Run on any machine with Azure CLI access:
-
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,servicePrincipalProfilefields fromaz aks show). - Any identities configured in your IaC (ARM/Bicep/Terraform) for cluster access.
- The AKS cluster’s managed identity or service principals (
- Decide which of these identities represent shared/broadly used accounts that should not have direct, unscoped access.
- List Azure AD app registrations and service principals used for AKS access (run from any machine with Azure AD Graph/MS Graph permissions):
-
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 widekubectl get rolebindings --all-namespaces -o wide
- For more detail on subjects:
kubectl get clusterrolebindings -o yamlkubectl get rolebindings --all-namespaces -o yaml
- Manually inspect subjects and look for:
system:anonymous,system:unauthenticated, orsystem:masters.system:serviceaccount:default:defaultor 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.
- Get current role bindings and cluster role bindings (any machine with kubectl access):
-
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 widekubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\n"}{end}'
- Identify applications using
defaultor other shared service accounts instead of dedicated ones. Decide which namespaces/workloads require creation and use of dedicated service accounts and corresponding Role/RoleBindings.
- List service accounts by namespace (any machine with kubectl access):
-
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
ServiceAccountobjects per application or CI/CD component. Role/ClusterRoleandRoleBinding/ClusterRoleBindingobjects that bind those service accounts and AAD groups with least-privilege access.
- Dedicated
- 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.
- Using Azure Portal, Azure CLI, or your IaC:
-
Verify that access is now via dedicated service accounts and AAD identities, and not via shared/default identities
- Re-run:
kubectl get clusterrolebindings -o yamlkubectl get rolebindings --all-namespaces -o yamlkubectl 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
defaultor shared accounts. - Any previously identified broad/shared or default identities have been removed or re-scoped according to your access model.
- Re-run:
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