Bypassing Entra ID Conditional Access policies for private AKS cluster deployment using Azure Managed Identity and self-hosted runners
This proof of concept validates that Azure Managed Identity bypasses Entra ID conditional access (CA) policies when deploying private AKS clusters.
Organizations with strict CA location policies can use managed identity to avoid authentication failures that occur with service principals. The PoC deploys a fully private AKS cluster from a self-hosted GitHub Actions runner inside the same VNet, demonstrating end-to-end private connectivity and CA bypass.
Service principal authentication during AKS creation is blocked by conditional access location policies.
When AKS Resource Provider authenticates using a service principal's credentials during az aks create, the sign-in originates from Azure datacenter IPs, not from the customer's network. If the organization enforces conditional access policies that restrict authentication to known perimeter IPs, these policies block the service principal sign-in.
SERVICE PRINCIPAL FLOW (PROBLEMATIC):
Runner VM → az login --service-principal → login.microsoftonline.com (from Runner IP ✓)
Runner VM → az aks create → ARM → AKS RP → login.microsoftonline.com (from Azure datacenter IP ✗)
↑ BLOCKED by CA
MANAGED IDENTITY FLOW (RECOMMENDED):
Runner VM → az login --identity → IMDS 169.254.169.254 (internal, no CA ✓)
Runner VM → az aks create → ARM → AKS RP → Azure fabric token (internal, no CA ✓)
↑ NOT evaluated by CA
The distinction is architectural: managed identities do not trigger conditional access because their credentials are managed by Azure and token issuance happens within the Azure fabric. There is no "source IP" for CA to evaluate.
Managed identity bypasses conditional access entirely — tokens are acquired internally via IMDS, not through login.microsoftonline.com.
"Managed identities aren't covered by policy." — Microsoft documentation on Conditional Access for workload identities
MI tokens are acquired via 169.254.169.254 — an internal Azure fabric endpoint. No external sign-in occurs.
The Conditional Access engine does not evaluate managed identity token requests at all. No source IP is exposed for evaluation.
A self-hosted runner VM in the same VNet as the AKS cluster ensures private API access and IMDS-based authentication.
Three-job workflow across two runner types: GitHub-hosted for infra provisioning, self-hosted for private AKS deployment.
graph TD
A["GitHub workflow_dispatch"] -->|ubuntu-latest| B["Job 1: Setup Infrastructure"]
B --> B1["Create VNet\nsubnet-aks + subnet-runner"]
B1 --> B2["Create MI + RBAC"]
B2 --> B3["Create Runner VM\nin subnet-runner"]
B3 --> B4["Register GH Actions Runner"]
B4 -->|self-hosted runner| C["Job 2: Deploy + Validate"]
C --> C1["az login --identity via IMDS"]
C1 --> C2["az aks create\n--enable-private-cluster\ninto subnet-aks"]
C2 --> C3{"Deploy OK?"}
C3 -->|Yes| C4["kubectl get nodes\nvia private endpoint"]
C4 --> C5["Log IPs + Upload Artifacts"]
C3 -->|No| C5
C5 --> C6["Wait N minutes"]
C6 --> C7["Delete AKS RG"]
C7 -->|ubuntu-latest| D["Job 3: Teardown"]
D --> D1["Deregister Runner"]
D1 --> D2["Delete Infra RG"]
style A fill:#1e1b4b,stroke:#4f46e5,color:#e6edf3
style B fill:#1e3a5f,stroke:#3b82f6,color:#e6edf3
style C fill:#3b0764,stroke:#7c3aed,color:#e6edf3
style D fill:#4a1530,stroke:#e11d48,color:#e6edf3
style C3 fill:#78350f,stroke:#d97706,color:#e6edf3
style C4 fill:#064e3b,stroke:#059669,color:#e6edf3
graph LR
subgraph VNET["Shared VNet — 10.224.0.0/16"]
direction LR
subgraph SAKS["subnet-aks\n10.224.0.0/24"]
AKS["AKS Private Cluster\nAPI: 10.224.0.4\nNode: 10.224.0.5"]
end
subgraph SRUNNER["subnet-runner\n10.224.1.0/24"]
RUNNER["Runner VM\n10.224.1.4"]
end
RUNNER -->|"private endpoint"| AKS
end
RUNNER -->|"IMDS\n169.254.169.254"| FABRIC["Azure Fabric\nToken Issuance"]
style VNET fill:#0c1929,stroke:#3b82f6,stroke-width:2px,color:#e6edf3
style SAKS fill:#1a0a2e,stroke:#7c3aed,color:#e6edf3
style SRUNNER fill:#0a2e1e,stroke:#059669,color:#e6edf3
style FABRIC fill:#1e1b4b,stroke:#4f46e5,color:#e6edf3
graph LR
A["Runner VM"] -->|"az login --identity"| B["IMDS\n169.254.169.254"]
B -->|"MI token"| C["Azure Fabric"]
A -->|"az aks create"| D["ARM"]
D --> E["AKS RP"]
E -->|"Cluster MI via IMDS"| C
style A fill:#0a2e1e,stroke:#059669,color:#e6edf3
style B fill:#1e1b4b,stroke:#4f46e5,color:#e6edf3
style C fill:#064e3b,stroke:#059669,stroke-width:2px,color:#e6edf3
style D fill:#1e293b,stroke:#64748b,color:#e6edf3
style E fill:#1e293b,stroke:#64748b,color:#e6edf3
Fully automated infrastructure lifecycle — provision, deploy, validate, and teardown in a single workflow run.
A three-job workflow triggered by workflow_dispatch:
Runs on ubuntu-latest via OIDC. Creates shared VNet with two subnets, provisions managed identity with RBAC, creates runner VM, and registers it as a self-hosted runner.
Runs on the self-hosted runner. Authenticates via managed identity (IMDS), deploys private AKS into subnet-aks, validates with kubectl, logs IPs, and uploads artifacts.
Runs on ubuntu-latest. Deregisters the runner, deletes AKS and infrastructure resource groups. Always runs, even on failure.
A manually triggered workflow that scans for resource groups matching rg-aks-poc-* older than 45 minutes. Acts as a safety net for orphaned resources from failed runs.
. ├── .github/ │ └── workflows/ │ ├── deploy-private-aks.yml # Main deploy + log + teardown workflow │ └── cleanup-safety-net.yml # Safety net for orphaned resources ├── docs/ │ ├── index.html # This page (English) │ └── index.fr.html # French version ├── scripts/ │ ├── setup-runner-vm.sh # One-time: provision runner VM + MI │ ├── teardown-runner-vm.sh # One-time: delete runner VM │ ├── deploy-private-aks.sh # Standalone AKS deployment (reusable) │ └── log-ips.sh # IP logging utility ├── README.md # English documentation └── README.fr.md # French documentation
All 3 jobs succeeded. PoC objectives confirmed. Workflow run #23919580744
| Job | Runner | Duration | Result |
|---|---|---|---|
| setup-runner | ubuntu-latest | 7 min | ✅ Success |
| deploy-and-log | self-hosted (in-VNet) | 40 min (incl. 30 min wait) | ✅ Success |
| teardown-runner | ubuntu-latest | 18 sec | ✅ Success |
The Azure Activity Log confirms the az aks create ARM write operation originated from IP 20.104.78.99 — the runner VM's own public IP. Authentication happened via IMDS, not through login.microsoftonline.com. No CA evaluation was triggered.
Activity Log excerpt: Microsoft.ContainerService/managedClusters/write Accepted ClientIp: 20.104.78.99 Microsoft.ContainerService/managedClusters/write Started ClientIp: 20.104.78.99
enablePrivateCluster : true privateFqdn : aks-poc-23-...privatelink.canadacentral.azmk8s.io API Server Endpoint : https://...privatelink.canadacentral.azmk8s.io:443 Private FQDN resolves: 10.224.0.4 (private IP within the VNet)
kubectl cluster-info → Kubernetes control plane running at ...privatelink.canadacentral.azmk8s.io:443 kubectl get nodes → 1 node, Ready, v1.34.4 kubectl get pods -n kube-system → 15 pods, all Running kubectl get namespaces → default, kube-node-lease, kube-public, kube-system nslookup private FQDN → 10.224.0.4 ✓
| Log File | Content |
|---|---|
| runner-network.log | Runner VM public/private IP, hostname, subnet |
| aks-create.log | Full az aks create output (9 KB) |
| aks-cluster-info.log | Cluster properties (version, FQDN, network config) |
| kubectl-validation.log | All kubectl output including DNS resolution |
| ip-activity-log.log | Azure Activity Log ARM operation caller IPs |
| ip-signin-log.log | Entra sign-in query (expected 403 without P1/P2) |
Get the PoC running in your subscription in minutes.
Contributor + User Access Administrator at subscription scopeGH_PAT secret) with repo scopemain branchContributor + User Access Administrator roles at subscription scopeAZURE_CLIENT_ID — App registration client IDAZURE_TENANT_ID — Entra ID tenant IDAZURE_SUBSCRIPTION_ID — Target subscription IDGH_PAT — GitHub PAT with repo scope⚠️ Important: The workflow's Job 3 always runs (even on failure) and cleans up both the AKS and infrastructure resource groups. Manual cleanup is only needed if the workflow itself is cancelled before Job 3 executes.
Each 30-minute PoC run costs approximately $0.05 to $0.08 with a single Standard_B2s node on the Free tier AKS control plane. The runner VM runs only for the duration of the workflow and is automatically deleted.
Official documentation and source repository.
| Resource | Link |
|---|---|
| Source Repository | aks-private-deployment |
| Azure Private AKS Clusters | learn.microsoft.com |
| Use Managed Identity with AKS | learn.microsoft.com |
| Conditional Access for Workload Identities | learn.microsoft.com |