Proof of Concept — Azure Kubernetes Service

Private AKS Deployment with Managed Identity

Bypassing Entra ID Conditional Access policies for private AKS cluster deployment using Azure Managed Identity and self-hosted runners

📅 April 2, 2026

🇫🇷 Français

01 Overview

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.

$0.08
Cost per 30-min run
3
Automated jobs
0
CA evaluations triggered
100%
Private API access

02 Problem Statement

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.

Authentication Flow Comparison

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.

03 Solution

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
🔐

IMDS Token Acquisition

MI tokens are acquired via 169.254.169.254 — an internal Azure fabric endpoint. No external sign-in occurs.

🛡️

CA Engine Bypass

The Conditional Access engine does not evaluate managed identity token requests at all. No source IP is exposed for evaluation.

🌐

In-VNet Runner

A self-hosted runner VM in the same VNet as the AKS cluster ensures private API access and IMDS-based authentication.

04 Architecture

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
    

Network Architecture

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
    

Identity Flow

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
    

05 GitHub Actions Workflows

Fully automated infrastructure lifecycle — provision, deploy, validate, and teardown in a single workflow run.

deploy-private-aks.yml

A three-job workflow triggered by workflow_dispatch:

🏗️

Job 1: setup-runner

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.

🚀

Job 2: deploy-and-log

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.

🧹

Job 3: teardown-runner

Runs on ubuntu-latest. Deregisters the runner, deletes AKS and infrastructure resource groups. Always runs, even on failure.

cleanup-safety-net.yml

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.

File Structure

.
├── .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

06 Verified Run — April 2, 2026

All 3 jobs succeeded. PoC objectives confirmed. Workflow run #23919580744

Job Execution

JobRunnerDurationResult
setup-runnerubuntu-latest7 min✅ Success
deploy-and-logself-hosted (in-VNet)40 min (incl. 30 min wait)✅ Success
teardown-runnerubuntu-latest18 sec✅ Success

Finding 1: Managed Identity Bypasses Conditional Access

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

Finding 2: Private Cluster is Truly Private

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)

Finding 3: In-VNet Runner Reaches Private API Server

10.224.1.4
Runner VM
subnet-runner
10.224.0.4
API Server
private endpoint
10.224.0.5
AKS Node
subnet-aks
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 ✓

Finding 4: Artifacts Uploaded

Log FileContent
runner-network.logRunner VM public/private IP, hostname, subnet
aks-create.logFull az aks create output (9 KB)
aks-cluster-info.logCluster properties (version, FQDN, network config)
kubectl-validation.logAll kubectl output including DNS resolution
ip-activity-log.logAzure Activity Log ARM operation caller IPs
ip-signin-log.logEntra sign-in query (expected 403 without P1/P2)

07 Quick Start

Get the PoC running in your subscription in minutes.

Prerequisites

Setup Steps

  1. Create an Azure AD app registration with OIDC federated credentials for the main branch
  2. Assign Contributor + User Access Administrator roles at subscription scope
  3. Add GitHub Actions secrets:
    • AZURE_CLIENT_ID — App registration client ID
    • AZURE_TENANT_ID — Entra ID tenant ID
    • AZURE_SUBSCRIPTION_ID — Target subscription ID
    • GH_PAT — GitHub PAT with repo scope
  4. Trigger the deploy-private-aks workflow from the GitHub Actions UI

⚠️ 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.

Cost Estimate

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.

08 References

Official documentation and source repository.

ResourceLink
Source Repositoryaks-private-deployment
Azure Private AKS Clusterslearn.microsoft.com
Use Managed Identity with AKSlearn.microsoft.com
Conditional Access for Workload Identitieslearn.microsoft.com