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