securely deploy to a private Azure Kubernetes Service (AKS) cluster from CI/CD pipelines

To securely deploy to a private Azure Kubernetes Service (AKS) cluster from CI/CD pipelines, public cloud-hosted runners (such as default GitHub-hosted runners or Azure DevOps Microsoft-hosted agents) cannot be used directly because they lack access to the cluster’s Private Endpoint.

Instead, you must deploy self-hosted agents or runners inside a Virtual Network (VNet) that can resolve and reach the private AKS API server.

Deployment Architecture

Self-hosted runners poll GitHub or Azure DevOps over outbound HTTPS (port 443), eliminating the need for inbound open firewall ports. Because the agents live inside the Azure VNet, they resolve the private API server DNS via the Private DNS Zone link (privatelink.<region>.azmk8s.io).

Pattern 1: Azure Container Instances (ACI) Self-Hosted Agents

Deploying runners inside Azure Container Instances (ACI) injected into a delegated subnet in your spoke VNet is a lightweight, serverless approach for managing CI/CD infrastructure.

1. Terraform for VNet-Integrated ACI Agent

This configuration deploys an Azure DevOps pipeline agent into a VNet subnet with access to the private AKS DNS:

Terraform

# User-Assigned Managed Identity for the Agent
resource "azurerm_user_assigned_identity" "agent_identity" {
name = "id-ado-runner-prod"
location = var.location
resource_group_name = var.resource_group_name
}
# Grant RBAC on AKS Cluster to the Agent Identity
resource "azurerm_role_assignment" "agent_aks_rbac" {
scope = var.aks_cluster_id
role_definition_name = "Azure Kubernetes Service Cluster User"
principal_id = azurerm_user_assigned_identity.agent_identity.principal_id
}
# Container Group running in Delegated Subnet
resource "azurerm_container_group" "ado_agent" {
name = "aci-ado-agent-pool"
location = var.location
resource_group_name = var.resource_group_name
os_type = "Linux"
subnet_ids = [var.agent_subnet_id]
ip_address_type = "Private"
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.agent_identity.id]
}
container {
name = "azdevops-agent"
image = "mcr.microsoft.com/azure-pipelines/vso-agent:latest"
cpu = "2.0"
memory = "4.0"
environment_variables = {
AZP_URL = "https://dev.azure.com/YourOrganization"
AZP_POOL = "Private-AKS-Pool"
AZP_AGENT_NAME = "aci-agent-01"
}
secure_environment_variables = {
AZP_TOKEN = var.azure_devops_pat # Personal Access Token with Agent Pools (Read & Manage) scope
}
}
}

Pattern 2: Actions Runner Controller (ARC) inside AKS

If you are using GitHub Actions, the enterprise best practice is to run Actions Runner Controller (ARC). ARC runs directly inside Kubernetes (either inside a dedicated management AKS cluster or a separate namespace in your non-prod cluster) and dynamically scales ephemeral runner pods up and down based on workflow demand.

GitHub.com ──(Long-Polling)──► ARC Listener Pod ──► Scales Runner Pods inside VNet ──► Internal Private AKS API
Steps to Deploy ARC for GitHub Actions

1.Install ARC via Helm:Step 1.

Install the Actions Runner Controller operator using Helm on a cluster residing within the target VNet:

helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
helm repo update
# Install the ARC Controller
helm upgrade --install arc \
--namespace arc-systems \
--create-namespace \
actions-runner-controller/gha-runner-scale-set-controller

2.Configure GitHub Authentication:Step 2.

Create a Kubernetes secret containing your GitHub Personal Access Token (PAT) or GitHub App credentials:

kubectl create namespace arc-runners
kubectl create secret generic github-app-secret \
--namespace arc-runners \
--from-literal=github_app_id="123456" \
--from-literal=github_app_installation_id="789012" \
--from-file=github_app_private_key=./github-app.private-key.pem

3.Deploy Ephemeral Auto-Scaling Runner Scale Set:Step 3.

Deploy an autoscaling runner scale set that targets your GitHub repository or organization:

Bash

helm upgrade --install private-aks-runners \
--namespace arc-runners \
actions-runner-controller/gha-runner-scale-set \
--set githubConfigUrl="https://github.com/YourOrg/YourRepo" \
--set githubConfigSecret="github-app-secret" \
--set minRunners=1 \
--set maxRunners=10

Workflow Example: Passwordless Authentication via OIDC & Entra Workload Identity

Avoid embedding static credentials or service principal keys in your pipelines. Use OpenID Connect (OIDC) so the agent exchanges its identity token for an Azure Entra access token dynamically.

GitHub Actions Workflow (.github/workflows/deploy.yml)
name: Deploy to Private AKS
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: self-hosted # Directs job to your private self-hosted ARC runner
permissions:
id-token: write # Required for OIDC Entra authentication
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Log in to Azure via OIDC Federation
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Set AKS Cluster Context
uses: azure/aks-set-context@v4
with:
cluster-name: 'aks-corp-prod-eastus-001'
resource-group: 'rg-aks-prod-001'
use-kubelogin: 'true'
- name: Deploy Workload to Private Cluster
run: |
kubectl apply -f k8s/deployment.yaml
kubectl rollout status deployment/myapp-deployment

Comparison: ACI vs. ARC (K8s-Native)

FeatureAzure Container Instances (ACI)Actions Runner Controller (ARC)
Best Used ForAzure DevOps Pipelines, lightweight setupsGitHub Actions, enterprise-scale auto-scaling
MaintenanceMinimal (Serverless container runtime)Managed via Helm charts on Kubernetes
Scaling VelocityModerate (Starts new ACI instance per job)High (Pre-warmed pods start in seconds)
Network FootprintDirect VNet Integration via Delegated SubnetRuns natively inside the VNet cluster

securely test access to a private Azure Kubernetes Service (AKS) cluster,

To securely test access to a private Azure Kubernetes Service (AKS) cluster, your local admin tools (az CLI, kubectl) need a network route capable of resolving the cluster’s Private Endpoint FQDN (*.privatelink.<region>.azmk8s.io).

There are two primary patterns: Azure Bastion Tunneling / Jump Host and Azure Point-to-Site (P2S) VPN.

Pattern 1: Azure Bastion (Native Tunneling or Jump Host)

Azure Bastion allows secure access without needing public IP addresses exposed on your virtual machines.

Local Admin Machine ──(CLI Tunnel / SSH)──► Azure Bastion ──► VNet / Private DNS ──► Private AKS API Endpoint
Option A: Direct Local Access via Native CLI Tunneling (No Jump Host VM needed)

If Azure Bastion is configured on standard or developer SKU, you can tunnel API server traffic directly from your local terminal.

  1. Retrieve the cluster credentials locally:
    az aks get-credentials --resource-group rg-aks-prod-001 --name aks-corp-prod-eastus-001
  2. Open a Bastion tunnel to the private AKS API server:
    az aks bastion --resource-group rg-aks-prod-001 \ --name aks-corp-prod-eastus-001 \ --bastion-name bastion-hub-001 \ --bastion-resource-group rg-hub-networking
  3. Run kubectl get nodes in a separate terminal tab to manage your private cluster.
Option B: Using a Dedicated Jump Host VM

If your team enforces jump host bastion patterns:

  1. Provision a lightweight Linux or Windows VM in the spoke VNet or an aligned hub VNet linked to the AKS Private DNS Zone (privatelink.<region>.azmk8s.io).
  2. Connect via Bastion:
    az network bastion ssh --name bastion-hub-001 \ --resource-group rg-hub-networking \ --target-resource-id "/subscriptions/.../virtualMachines/vm-jumphost" \ --auth-type ssh-key \ --username azureuser \ --ssh-key ~/.ssh/id_rsa
  3. Install Azure CLI & kubectl on the Jump Host:
    sudo az aks install-cli az login az aks get-credentials --resource-group rg-aks-prod-001 --name aks-corp-prod-eastus-001 kubectl get nodes

Pattern 2: Azure Point-to-Site (P2S) VPN

A Point-to-Site VPN connects your workstation directly to the Azure Virtual Network containing the AKS cluster or Private Endpoint.

Developer Workstation ──(Encrypted TLS/OpenVPN)──► Azure Virtual Network Gateway ──► Private AKS API Endpoint

Configuration Steps

1.Deploy VPN Gateway:

Deploy an Azure Virtual Network Gateway (VPN type: Route-based) in your Hub VNet’s GatewaySubnet.

2.Configure P2S Authentication:

Configure Point-to-Site parameters:

  • Address pool: E.g., 172.16.201.0/24 (Client IP pool).
  • Authentication type: Microsoft Entra ID (Azure AD) or Azure-generated Certificate.
  • Tunnel type: OpenVPN (SSL/TLS).

3.Configure DNS Resolution:

Ensure your client machine can resolve the AKS Private Endpoint:

  • Option 1 (Azure Private DNS Resolver): Point the VPN client DNS settings to an Azure Private DNS Resolver deployed in the VNet.
  • Option 2 (Custom Hosts File / Client DNS): Manually map the Private Endpoint IP to the API FQDN (10.x.x.x cluster-dns-name.privatelink.eastus.azmk8s.io) on your test machine for validation testing.

4.Connect & Validate:

  1. Download and import the VPN profile into the Azure VPN Client.
  2. Authenticate using Entra ID.
  3. Test endpoint resolution and cluster connectivity directly from your native workstation:

Bash

nslookup aks-corp-prod-eastus-001-dns.privatelink.eastus.azmk8s.io
kubectl get nodes

Network Resolution & Connectivity Verification

Regardless of the approach, perform these troubleshooting steps if kubectl commands time out:

Check ItemValidation CommandExpected Result
1. DNS Resolutionnslookup <private-cluster-fqdn>Resolves to a Private IP (e.g., 10.x.x.x), NOT a public IP address.
2. Port 443 AccessTest-NetConnection -ComputerName <fqdn> -Port 443TcpTestSucceeded: True
3. Private DNS LinkPortal: Private DNS Zone > Virtual Network LinksVNet containing the Bastion/VPN must be linked to privatelink.<region>.azmk8s.io.

production-ready Terraform module configured

Here is a production-ready Terraform module configured according to Azure Landing Zone (ALZ) principles and Azure Verified Modules (AVM) standards.

This module provisions an AKS cluster deployed into an existing landing zone spoke network with:

  • Private Cluster Architecture (API Server isolated via Private Link).
  • Azure CNI Overlay with Cilium for zero-trust pod network policies.
  • Entra ID Workload Identity & Azure RBAC Integration (local accounts disabled).
  • Azure Policy for Kubernetes (enforces Pod Security Standards).
  • Host-based Encryption & Availability Zones enabled across node pools.

Module Architecture

Plaintext

├── main.tf # AKS Cluster, Identity, DNS, and RBAC resources
├── variables.tf # Module input variables
├── outputs.tf # Cluster IDs, Principal IDs, and Private FQDN outputs

main.tf

Terraform

terraform {
required_version = ">= 1.9.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
# -----------------------------------------------------------------------------
# 1. User-Assigned Managed Identity for AKS Control Plane
# -----------------------------------------------------------------------------
resource "azurerm_user_assigned_identity" "aks_identity" {
name = "id-${var.cluster_name}-control-plane"
location = var.location
resource_group_name = var.resource_group_name
tags = var.tags
}
# Grant Network Contributor on the Spoke Subnet for CNI IP Allocation
resource "azurerm_role_assignment" "aks_network_contributor" {
scope = var.subnet_id
role_definition_name = "Network Contributor"
principal_id = azurerm_user_assigned_identity.aks_identity.principal_id
}
# Grant Private DNS Zone Contributor for Private Cluster DNS registration
resource "azurerm_role_assignment" "aks_dns_contributor" {
count = var.private_dns_zone_id != null ? 1 : 0
scope = var.private_dns_zone_id
role_definition_name = "Private DNS Zone Contributor"
principal_id = azurerm_user_assigned_identity.aks_identity.principal_id
}
# -----------------------------------------------------------------------------
# 2. Managed Kubernetes Cluster (AKS)
# -----------------------------------------------------------------------------
resource "azurerm_kubernetes_cluster" "aks" {
name = var.cluster_name
location = var.location
resource_group_name = var.resource_group_name
dns_prefix = var.cluster_name
kubernetes_version = var.kubernetes_version
local_account_disabled = true # Enforce Entra ID Auth only
# --- Security & Private Endpoint Patterns ---
private_cluster_enabled = true
private_dns_zone_id = var.private_dns_zone_id
private_cluster_public_fqdn_enabled = false
# --- Workload Identity & OIDC Issuer ---
oidc_issuer_enabled = true
workload_identity_enabled = true
# --- Control Plane Identity ---
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.aks_identity.id]
}
# --- Default System Node Pool (Hardened Infrastructure) ---
default_node_pool {
name = "system"
node_count = var.system_node_count
vm_size = var.system_vm_size
vnet_subnet_id = var.subnet_id
zones = ["1", "2", "3"]
enable_auto_scaling = true
min_count = 3
max_count = 6
# Node Security Configuration
enable_host_encryption = true
os_disk_type = "Ephemeral"
os_disk_size_gb = 64
only_critical_addons_enabled = true # Keep user apps off system nodes
node_labels = {
"environment" = var.environment
"role" = "system"
}
}
# --- Advanced Networking (Azure CNI Overlay + Cilium) ---
network_profile {
network_plugin = "azure"
network_plugin_mode = "overlay"
network_policy = "cilium"
ebpf_data_path = "cilium"
outbound_type = "userDefinedRouting" # Route outbound via ALZ Hub Firewall
pod_cidr = "192.168.0.0/16"
service_cidr = "172.16.0.0/16"
dns_service_ip = "172.16.0.10"
}
# --- Azure Entra ID & Kubernetes RBAC Integration ---
azure_active_directory_role_based_access_control {
tenant_id = var.tenant_id
admin_group_object_ids = var.admin_group_object_ids
azure_rbac_enabled = true
}
# --- Security Baseline Add-ons ---
azure_policy_enabled = true # OPA Gatekeeper for Pod Security Standards
key_vault_secrets_provider {
secret_rotation_enabled = true
}
oms_agent {
log_analytics_workspace_id = var.log_analytics_workspace_id
}
tags = var.tags
depends_on = [
azurerm_role_assignment.aks_network_contributor,
azurerm_role_assignment.aks_dns_contributor
]
}
# -----------------------------------------------------------------------------
# 3. User Workload Node Pool
# -----------------------------------------------------------------------------
resource "azurerm_kubernetes_cluster_node_pool" "user" {
name = "userpool1"
kubernetes_cluster_id = azurerm_kubernetes_cluster.aks.id
vm_size = var.user_vm_size
vnet_subnet_id = var.subnet_id
zones = ["1", "2", "3"]
enable_auto_scaling = true
min_count = var.user_node_min_count
max_count = var.user_node_max_count
enable_host_encryption = true
os_disk_type = "Ephemeral"
os_disk_size_gb = 128
node_labels = {
"role" = "workload"
}
tags = var.tags
}

variables.tf

Terraform

variable "cluster_name" {
type = string
description = "The name of the AKS cluster."
}
variable "resource_group_name" {
type = string
description = "Target Resource Group inside the Workload Landing Zone."
}
variable "location" {
type = string
description = "Azure region for cluster deployment."
}
variable "environment" {
type = string
description = "Deployment environment (e.g., prod, non-prod)."
default = "prod"
}
variable "kubernetes_version" {
type = string
description = "Kubernetes version."
default = "1.30"
}
variable "subnet_id" {
type = string
description = "The Resource ID of the delegated AKS subnet in the Spoke VNet."
}
variable "private_dns_zone_id" {
type = string
description = "Resource ID of the Private DNS Zone (privatelink.<region>.azmk8s.io) linked to the Hub/Spoke VNets."
default = null
}
variable "tenant_id" {
type = string
description = "Azure Entra ID Tenant ID for RBAC integration."
}
variable "admin_group_object_ids" {
type = list(string)
description = "Entra ID Group Object IDs allowed to act as Cluster Admins."
}
variable "log_analytics_workspace_id" {
type = string
description = "Central Log Analytics Workspace ID for AKS audit and Container Insights logs."
}
variable "system_node_count" {
type = number
default = 3
}
variable "system_vm_size" {
type = string
default = "Standard_D4s_v5"
}
variable "user_vm_size" {
type = string
default = "Standard_D8s_v5"
}
variable "user_node_min_count" {
type = number
default = 3
}
variable "user_node_max_count" {
type = number
default = 10
}
variable "tags" {
type = map(string)
description = "Resource tags."
default = {}
}

outputs.tf

Terraform

output "aks_cluster_id" {
value = azurerm_kubernetes_cluster.aks.id
description = "The Resource ID of the deployed AKS cluster."
}
output "aks_private_fqdn" {
value = azurerm_kubernetes_cluster.aks.private_fqdn
description = "The Private FQDN of the Kubernetes API Server."
}
output "oidc_issuer_url" {
value = azurerm_kubernetes_cluster.aks.oidc_issuer_url
description = "The OIDC Issuer URL for configuring Entra Workload Identity federated credentials."
}
output "aks_control_plane_identity_principal_id" {
value = azurerm_user_assigned_identity.aks_identity.principal_id
description = "Principal ID of the AKS control plane managed identity."
}

How to Instantiate this Module in your Landing Zone Root

Terraform

module "secure_aks_landing_zone" {
source = "./modules/aks-private-landing-zone"
cluster_name = "aks-corp-prod-eastus-001"
resource_group_name = "rg-aks-prod-001"
location = "eastus"
environment = "prod"
kubernetes_version = "1.30"
# Networking & DNS from Spoke Landing Zone Module
subnet_id = "/subscriptions/<sub-id>/resourceGroups/rg-spoke-networking/providers/Microsoft.Network/virtualNetworks/vnet-spoke-corp/subnets/snet-aks-nodes"
private_dns_zone_id = "/subscriptions/<hub-sub-id>/resourceGroups/rg-hub-dns/providers/Microsoft.Network/privateDnsZones/privatelink.eastus.azmk8s.io"
# Governance & Monitoring
tenant_id = data.azurerm_client_config.current.tenant_id
admin_group_object_ids = ["<entra-id-sec-ops-group-object-id>"]
log_analytics_workspace_id = "/subscriptions/<mgmt-sub-id>/resourceGroups/rg-alz-management/providers/Microsoft.OperationalInsights/workspaces/law-alz-central"
tags = {
BusinessUnit = "CoreInfrastructure"
Environment = "Production"
ManagedBy = "Terraform"
}
}

Securing Azure Kubernetes Service (AKS)

Securing Azure Kubernetes Service (AKS) requires a defense-in-depth approach spanning cluster management, network architecture, identity controls, application runtime, and data protection.

1. Identity & Access Management (IAM)

  • Integrate Microsoft Entra ID with Kubernetes RBAC: Bind Entra ID users and groups to Kubernetes roles using Azure RBAC for Kubernetes Authorization. Avoid sharing direct cluster-admin certificates (az aks get-credentials --admin).
  • Workload Identity (No Static Keys): Migrate from legacy pod identities to Entra Workload Identity. Workloads authenticate to Azure resources (Key Vault, Storage, Cosmos DB) via short-lived OIDC tokens without handling secret keys.
  • Privileged Access & PIM: Require Just-In-Time (JIT) access via Privileged Identity Management (PIM) for cluster administrators.

2. Network Isolation & Perimeter Controls

  • Private AKS Clusters: Lock down the API server by deploying a Private Cluster. Access the management endpoint via VPN, ExpressRoute, or a Bastion jump host. If a public API server is required, restrict access using API Server Authorized IP Ranges.
  • Zero-Trust Pod Networking: Enable Azure CNI powered by Cilium or standard Azure CNI with Network Policies (Calico/Cilium). Apply a default-deny-all network policy to prevent unauthorized east-west lateral movement between namespaces.
  • Controlled Egress & Ingress: Route all outbound node/pod traffic through an Azure Firewall or NVA using User Defined Routes (UDR) to enforce FQDN filtering. Terminate inbound HTTP/HTTPS traffic at an Application Gateway WAF or Azure Front Door.

3. Node & Cluster Infrastructure Hardening

  • Use Host-Based Encryption & Confidential VMs: Encrypt OS and temp disks at the host level using Customer-Managed Keys (CMK). For highly sensitive workloads, leverage Confidential Compute node pools (AMD SEV-SNP / Intel SGX).
  • Automate OS Patching: Enable Auto-Upgrade Profiles (node-image or stable) and Kured (Kubernetes Reboot Daemon) to apply security updates automatically to underlying Linux/Windows node OS images.
  • Disable Node SSH Access: Disable public SSH access to worker nodes. Use az aks node-shell for emergency diagnostic access.

4. Pod & Workload Governance

  • Enforce Pod Security Standards (PSS): Use the built-in Azure Policy for AKS (OPA Gatekeeper engine) to enforce the Restricted or Baseline pod security profile:
    • Block privileged containers.
    • Require containers to run with a non-root UID (runAsNonRoot).
    • Set root filesystems to read-only (readOnlyRootFilesystem).
    • Disallow allowPrivilegeEscalation.
  • Secrets Store CSI Driver: Integrate Azure Key Vault Provider for Secrets Store CSI Driver to mount secrets into pod volumes as files instead of storing sensitive data as standard base64 Kubernetes Secrets.

5. Container Image & Supply Chain Security

  • Scan & Sign Images: Connect Azure Container Registry (ACR) with Microsoft Defender for Containers to automatically scan images for CVEs at ingestion. Use Notary / Notation to digitally sign images and enforce image signature validation via policy before deployment.
  • Minimal Base Images: Build container images using minimal distroless base images to reduce the attack surface area and eliminate unnecessary shell tooling.

6. Observability & Threat Detection

  • Microsoft Defender for Containers: Enable Defender for Cloud on AKS for real-time threat detection (e.g., detecting crypto-mining pods, suspicious execution commands, or exposed dashboard endpoints).
  • Diagnostic Logging to Sentinel: Stream Kubernetes API audit logs, controller manager events, and system logs to a central Log Analytics Workspace connected to Microsoft Sentinel.

Automating Azure Policy

Automating Azure Policy allows organizations to transform cloud governance from a manual checklist into an enforceable Policy-as-Code (PaC) engineering process.

This guide outlines core enterprise best practices, a standard lifecycle, and automation patterns using Terraform, GitHub Actions, and Automated Remediation.

1. Enterprise Policy Lifecycle

Implementing policy changes direct to Deny mode in production will break deployments and disrupt developers. Enforce policy automation using a progressive phase-in lifecycle:

┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Phase 1: │ │ Phase 2: │ │ Phase 3: │ │ Phase 4: │
│ Write & Parameter│ ───► │ Deploy Audit │ ───► │ Auto-Remediate │ ───► │ Enforce Hard Deny │
│ (Policy-as-Code)│ │ (DoNotEnforce) │ │ (DINE / Modify) │ │ (Block Non-Compl) │
└─────────────────┘ └─────────────────┘ └──────────────────┘ └──────────────────┘
  • Audit First (DoNotEnforce / Audit): Deploy new policies in audit mode to collect compliance metrics without blocking pipelines or existing workloads.
  • Auto-Remediate (DeployIfNotExists / Modify): Automatically inject missing components (e.g., Log Analytics extensions, required tags, default diagnostic settings) during or post-deployment.
  • Hard Block (Deny): Once non-compliance is resolved across existing workloads, set parameter default values to Deny to prevent future drift.

2. Architectural Best Practices

  1. Always Parameterize the Policy Effect: Never hardcode "effect": "Deny" or "effect": "Audit" inside your JSON policy definitions. Parameterize effect so you can shift enforcement modes per environment (e.g., Audit in Dev/Sandbox, Deny in Prod) via the policy assignment without redefining the policy.
  2. Assign at Management Groups, Not Subscriptions: Assign policies at the Intermediate Root or archetype Management Groups (Corp, Online, Platform). Child subscriptions will inherit policies automatically.
  3. Group Policies into Initiatives (Policy Sets): Avoid single policy assignments. Package related definitions into Initiatives (e.g., CIS Benchmark Baseline, Cost Optimization Controls) to stay under Azure assignment limits and simplify reporting dashboards.
  4. Enforce Time-Bound Exemptions: When teams require exemptions, scope them as narrowly as possible (at Resource Group/Resource level) and set an explicit Expiration Date.

3. Policy-as-Code Implementation (Terraform)

Below is an enterprise Terraform configuration using the azurerm provider to deploy a parameterized custom policy, group it into an initiative, assign it to a Management Group, and configure a Managed Identity for automated remediation.

Terraform

# 1. Custom Policy Definition (Parameterized Effect)
resource "azurerm_policy_definition" "deny_public_ip" {
name = "deny-public-ip-custom"
policy_type = "Custom"
mode = "Indexed"
display_name = "Audit or Deny Public IP Addresses on NICs"
management_group_id = var.management_group_id
policy_rule = jsonencode({
if = {
field = "type"
equals = "Microsoft.Network/networkInterfaces"
}
then = {
effect = "[parameters('effect')]"
details = {
field = "Microsoft.Network/networkInterfaces/ipConfigurations[*].publicIpAddress.id"
exists = true
}
}
})
parameters = jsonencode({
effect = {
type = "String"
defaultValue = "Audit"
allowedValues = ["Audit", "Deny", "Disabled"]
metadata = {
displayName = "Effect"
description = "Enable or disable policy enforcement."
}
}
})
}
# 2. Initiative (Policy Set Definition)
resource "azurerm_policy_set_definition" "network_security_baseline" {
name = "network-security-baseline"
policy_type = "Custom"
display_name = "Network Security Governance Baseline"
management_group_id = var.management_group_id
policy_definition_reference {
policy_definition_id = azurerm_policy_definition.deny_public_ip.id
parameter_values = jsonencode({
effect = { value = "[parameters('publicIpEffect')]" }
})
}
parameters = jsonencode({
publicIpEffect = {
type = "String"
defaultValue = "Audit"
}
})
}
# 3. Policy Assignment with Managed Identity
resource "azurerm_management_group_policy_assignment" "assign_baseline" {
name = "assign-net-sec-baseline"
management_group_id = var.management_group_id
policy_set_definition_id = azurerm_policy_set_definition.network_security_baseline.id
location = var.location
# System-Assigned Identity required for DeployIfNotExists/Modify remediation
identity {
type = "SystemAssigned"
}
parameters = jsonencode({
publicIpEffect = { value = var.environment == "prod" ? "Deny" : "Audit" }
})
}

4. Automating Remediation Tasks for Existing Resources

When assigning policies with DeployIfNotExists or Modify effects, existing resources deployed prior to policy assignment remain non-compliant. To automatically fix existing resources at scale, trigger programmatic Remediation Tasks via Azure CLI or pipeline tasks:

Bash

# 1. Trigger an on-demand compliance scan across the target scope
az policy state trigger-scan --management-group "alz-landing-zones" --no-wait
# 2. Trigger automated remediation for non-compliant existing resources
az policy remediation create \
--name "remediate-missing-diag-settings-$(date +%Y%m%d)" \
--management-group "alz-landing-zones" \
--policy-assignment "assign-net-sec-baseline" \
--definition-reference-id "deploy-diag-settings"

5. CI/CD Validation Pipeline (GitHub Actions)

Add policy linter and static validation checks in your CI/CD pipelines to catch syntax errors or prohibited Deny configurations before merging PRs:

YAML

name: Azure Policy Guardrails Validation
on:
pull_request:
paths:
- 'policies/**'
jobs:
validate-policies:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Validate Azure Policy Rule Schemas
run: |
for file in policies/*.json; do
echo "Linting $file..."
# Use jq to validate valid JSON format
jq empty "$file"
done
- name: Terraform Plan & Security Check
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.0
- name: Terraform Init & Validate
run: |
terraform init -backend=false
terraform validate

How Microsoft Defender work with Microsoft Sentinel (SIEM/SOAR)

Microsoft Defender (XDR/Workload Protection) and Microsoft Sentinel (SIEM/SOAR) work together as a complementary, unified security operations stack.

While Defender operates at the domain level to detect and stop threats on specific workloads, Sentinel operates at the enterprise level to correlate data across your entire footprint (including non-Microsoft clouds, network devices, and third-party tools).

The Roles: Defender vs. Sentinel

  • Microsoft Defender (XDR & Defender for Cloud): Acts as your first line of defense. It automatically blocks malware, isolates infected hosts, terminates malicious processes, and correlates raw signals into domain-specific alerts (e.g., Endpoint, Email, Cloud Workloads).
  • Microsoft Sentinel: Acts as your centralized SOC cockpit. It aggregates logs and alerts from Defender alongside network logs (Palo Alto, Cisco), multi-cloud environments (AWS, GCP), and enterprise apps (Salesforce, SAP) to detect complex, multi-stage attacks across your entire estate.

1. Native Integration & Signal Sharing

Connecting Defender to Sentinel requires no complex pipelines or agent deployments—it uses built-in, 1-click connectors.

  • Bi-Directional Incident Sync: When Defender aggregates raw alerts into an incident, it automatically appears in Sentinel. If an analyst closes or updates an incident in Sentinel, that status updates in Defender in real time (and vice versa).
  • Raw Data Streaming (Advanced Hunting): Beyond high-level alerts, Defender streams raw telemetry events (process executions, network connections, sign-in attempts) into Sentinel’s Log Analytics workspace for long-term retention and cross-platform KQL querying.

2. Key Integration Use Cases

CapabilityHow They Work Together
Cross-Platform CorrelationDefender detects a phishing email click on an endpoint. Sentinel correlates that event with an unauthorized login on AWS or an anomalous file download in Salesforce.
Unified SOC PortalSecurity teams can manage incidents from both Defender and Sentinel inside a single interface (Microsoft Defender Portal), removing the need to context-switch between portals.
Automated Response (SOAR)Defender executes local remediation (e.g., quarantining an endpoint or resetting an Entra ID password). Sentinel triggers broader SOAR playbooks via Azure Logic Apps (e.g., creating a ServiceNow ticket, notifying Slack/Teams, or updating a firewall blocklist).
Cost OptimizationIngesting Microsoft Defender XDR alert data and incident metadata into Microsoft Sentinel is free of charge. (You only pay for underlying raw telemetry streaming if enabled for long-term retention).

3. How Data Flows Step-by-Step

1.Threat Detection in Defender: Domain Protection.

An attacker attempts a pass-the-hash attack on a domain controller while simultaneously sending spear-phishing emails to executive accounts.

  • Defender for Identity and Defender for Office 365 detect the activity, block the malicious emails, and group the alerts into a single Defender Incident.

2.Streaming to Microsoft Sentinel: Enterprise Correlation.

The Defender Incident streams automatically into Microsoft Sentinel via the native Microsoft Defender XDR connector.

  • Sentinel correlates the Defender incident with incoming logs from an on-premises Palo Alto firewall, showing that the attacker is also attempting SSH brute-force connections to an AWS EC2 instance.

3.Response & Remediation: Automated SOAR.

Sentinel fires an automated Logic App Playbook:

  1. Triggers Defender to isolate the compromised endpoint.
  2. Revokes the user’s Entra ID session tokens via PIM.
  3. Pushes the attacker’s malicious IP to the perimeter firewall blocklist.
  4. Automatically updates the incident status across both Sentinel and Defender.

SIEM – security information and event management

SOAR – (Security Orchestration, Automation, and Response)

troubleshoot Data Collection Rules (DCR) when logs fail to appear in Log Analytics

When logs fail to appear in a Log Analytics Workspace after configuring a Data Collection Rule (DCR), the root cause typically falls into one of four categories: missing resource association, agent/extension issues, network connectivity blocks, or RBAC/schema mismatches.

Follow these sequential steps to diagnose and isolate where the ingestion pipeline is breaking:

1.Verify Data Collection Rule Association (DCRA):1-2 minutes.

A DCR does not collect logs from a resource unless an explicit association exists.

  1. Go to Azure Monitor > Data Collection Rules and open your DCR.
  2. Click Resources under Configuration.
  3. Verify that your target Virtual Machine, Arc Server, or resource is listed.

Verification: If missing, click + Add and associate the resource. Wait 5 minutes for the DCR deployment payload to push down to the agent.

2.Check Agent Extension Status & Managed Identity:3-5 minutes.

For Azure Monitor Agent (AMA), the extension must be healthy and the VM’s Managed Identity active.

  1. Navigate to your target VM > Extensions + applications.
  2. Confirm that AzureMonitorWindowsAgent or AzureMonitorLinuxAgent is installed and shows Provisioning Succeeded.
  3. Ensure the VM has an active System-Assigned or User-Assigned Managed Identity enabled.

Verification: Run a KQL query in your Log Analytics workspace to check if heartbeats are arriving:

kql

Heartbeat
| where Computer == "YourComputerName"
| where Category == "Azure Monitor Agent"

If no heartbeat appears, the agent is either offline or blocked by local runtime errors.

3.Test Network & Data Collection Endpoint (DCE) Connectivity:5-10 minutes.

If the VM is behind a firewall, Network Security Group (NSG), or Private Link, the agent will fail to transmit data.

  1. Ensure the VM can reach the required Azure Monitor endpoints on port 443:
    • *.handler.control.monitor.azure.com
    • *.ingest.monitor.azure.com
  2. If using Azure Private Link, ensure a Data Collection Endpoint (DCE) is linked to both the DCR and your Private Link Scope (AMPLS).

Verification: Run a connectivity test directly from the VM:

PowerShell

# Windows PowerShell
Test-NetConnection -ComputerName "your-dce-endpoint.ingest.monitor.azure.com" -Port 443

Bash

# Linux Terminal
curl -v https://your-dce-endpoint.ingest.monitor.azure.com

4.Verify RBAC Permissions & Schema for Custom Logs/APIs:Custom Ingestion Checks.

If ingesting logs via the Logs Ingestion API or a custom table:

  1. Role Assignment: Ensure the sending application or service principal is assigned the Monitoring Metrics Publisher role on the DCR itself.
  2. Table Schema: Ensure the destination custom table (ending in _CL) exists in the Log Analytics workspace and matching fields are properly defined.
  3. Ingestion Time Lag: New custom tables or first-time DCR associations can take up to 15 minutes to process initial schemas and show output.

Verification: Query the _CL custom table directly or check the Azure Activity Log for 403 (Forbidden) authorization failures.

5.Run the On-Host AMA Troubleshooter Tool:Diagnostics.

If heartbeats and networking pass but specific event logs/syslogs still fail to appear, run the native troubleshooter script on the host machine:

  • Windows (PowerShell as Admin):

PowerShell

C:\Packages\Plugins\Microsoft.Azure.Monitor.AzureMonitorWindowsAgent\<version>\Troubleshooter\AgentTroubleshooter.exe --ama
  • Linux (Terminal as root):

Bash

sudo sh /var/lib/waagent/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent-<version>/ama_tst/ama_troubleshooter.sh

Verification: The script checks local configuration files, parses cached DCR JSON schemas, and outputs actionable failure codes (e.g., invalid XPath stream or corrupted datastore).
Azure Monitor Agent NOT Ingesting Logs & No Heartbeat? REAL FIX!

This video provides a practical, hands-on walkthrough for diagnosing Azure Monitor Agent log ingestion failures, verifying agent heartbeats, and resolving common DCR configuration issues.

Azure Monitor Agent (AMA) and Data Collection Rules (DCR)

Deploying the Azure Monitor Agent (AMA) and Data Collection Rules (DCR) involves a shift from legacy agents: DCRs define what data to collect and where to send it, while AMA acts as the lightweight transport mechanism installed on the virtual machine.

Deployment Architecture

┌────────────────────────────────────────────────────────┐
│ Data Collection Rule (DCR) │
│ • Data Sources (Event Logs, Syslog, Performance) │
│ • Destination (Log Analytics Workspace / Sentinel) │
└───────────────────────────┬────────────────────────────┘
│
│ Associated via DCRA
▼
┌────────────────────────────────────────────────────────┐
│ Virtual Machine │
│ Azure Monitor Agent Extension (AMA) │
│ (Windows: AzureMonitorWindowsAgent) │
│ (Linux: AzureMonitorLinuxAgent) │
└────────────────────────────────────────────────────────┘

Method 1: Using the Azure Portal (Recommended for Beginners)

The Azure Portal simplifies deployment by installing the agent and creating the DCR simultaneously.

1.Navigate to Data Collection Rules:Step 1.

  1. Search for Monitor in the top search bar.
  2. In the left menu under Settings, select Data Collection Rules.
  3. Click + Create.

2.Configure Basics:Step 2.

  1. Select your Subscription and Resource Group.
  2. Enter a Rule Name (e.g., dcr-windows-vm-monitoring).
  3. Select the target Platform Type (Windows or Linux).
  4. Set the Region (Must match the destination Log Analytics Workspace region).

3.Associate Virtual Machines:Step 3.

  1. Go to the Resources tab and click + Add resources.
  2. Select the target Virtual Machines or Azure Arc-enabled servers.

Note: When you associate a VM with a DCR, Azure automatically installs the Azure Monitor Agent extension on the target VM if it is not already present.

4.Add Data Sources and Destinations:Step 4.

  1. Go to the Collect and deliver tab and click + Add data source.
  2. Choose your source type:
    • Performance Counters: CPU, Memory, Disk, Network.
    • Windows Event Logs: System, Application, Security logs (Critical, Error, Warning, Information).
    • Syslog: Facility levels for Linux instances.
    • Custom Text Logs / IIS Logs: Paths to specific log files.
  3. On the Destination tab, select Azure Monitor Logs and pick your target Log Analytics Workspace.
  4. Select Review + create and click Create.

Method 2: Enterprise Scale via Azure Policy (Automated)

For enterprise environments, manual deployment leads to drift. Use Azure Policy initiatives to enforce AMA installation and DCR association automatically whenever a new VM is provisioned.

  1. Go to Azure Policy > Definitions.
  2. Search for the built-in initiative: Enable Azure Monitor for VMs with Azure Monitor Agent.
  3. Click Assign Initiative.
  4. Set the scope to your target Management Group or Subscription.
  5. In the Parameters tab, select your default Log Analytics Workspace and target Data Collection Rule.
  6. Create a Remediation Task to install AMA on pre-existing virtual machines across the environment.

Method 3: Scripted Deployment (Azure CLI / PowerShell)

If deploying via CI/CD pipelines, split the task into installing the extension and linking the DCR.

Option A: Install AMA Extension

Azure CLI

Bash

# Install on Windows VM
az vm extension set \
--name AzureMonitorWindowsAgent \
--publisher Microsoft.Azure.Monitor \
--resource-group myResourceGroup \
--vm-name myWindowsVM \
--enable-auto-upgrade true
# Install on Linux VM
az vm extension set \
--name AzureMonitorLinuxAgent \
--publisher Microsoft.Azure.Monitor \
--resource-group myResourceGroup \
--vm-name myLinuxVM \
--enable-auto-upgrade true

PowerShell

PowerShell

# Windows VM
Set-AzVMExtension `
-ResourceGroupName "myResourceGroup" `
-VMName "myWindowsVM" `
-Name "AzureMonitorWindowsAgent" `
-Publisher "Microsoft.Azure.Monitor" `
-ExtensionType "AzureMonitorWindowsAgent" `
-EnableAutomaticUpgrade $true
# Linux VM
Set-AzVMExtension `
-ResourceGroupName "myResourceGroup" `
-VMName "myLinuxVM" `
-Name "AzureMonitorLinuxAgent" `
-Publisher "Microsoft.Azure.Monitor" `
-ExtensionType "AzureMonitorLinuxAgent" `
-EnableAutomaticUpgrade $true
Option B: Associate VM with Existing DCR (DCRA)

Bash

# Create Data Collection Rule Association via Azure CLI
az monitor data-collection rule association create \
--name "myVM-DCR-Association" \
--resource "/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/myWindowsVM" \
--rule-id "/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Insights/dataCollectionRules/myDCRName"

Verification & Troubleshooting

  1. Check Extension Health: Navigate to your VM in the Azure Portal > Extensions + applications. Confirm AzureMonitorWindowsAgent or AzureMonitorLinuxAgent shows Provisioning succeeded.
  2. Query Ingested Logs: Navigate to your Log Analytics Workspace > Logs and run a simple KQL query to confirm heartbeat data is streaming:
    Code snippetHeartbeat | where Category == "Azure Monitor Agent" | summarize LastCall = max(TimeGenerated) by Computer, OSType
  3. Run On-VM Troubleshooter:
    • Windows: Run C:\Packages\Plugins\Microsoft.Azure.Monitor.AzureMonitorWindowsAgent\{version}\Troubleshooter\AgentTroubleshooter.exe --ama.
    • Linux: Execute sudo sh /var/lib/waagent/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent-{version}/ama_tst/ama_troubleshooter.sh.

Check out this Easily install Azure Monitor agent, Setup DCR and Deployment of agent via policy

video for a step-by-step walkthrough on creating Data Collection Rules and deploying the Azure Monitor Agent at scale using Azure Policy.

Azure Monitor

Azure Monitor is Microsoft’s comprehensive telemetry collection, monitoring, and observability service for Azure, hybrid, and multicloud resources. It provides end-to-end visibility across your entire technology stack—from infrastructure and networks down to application code and end-user experiences.

Architecture & Data Flow

Azure Monitor operates on a core telemetry pipeline that collects data from every layer of your environment, aggregates it into central datastores, and surfaces it through visualization, analytics, and automated alerting.

1. Core Data Types: Metrics vs. Logs

Azure Monitor segregates incoming telemetry into two primary formats:

Data TypeDescriptionPrimary Use CaseStorage & Retention
MetricsLightweight, numerical time-series values representing the state of a system at a specific point in time (e.g., CPU %, Available Memory, Request Rate).Near real-time alerting, health dashboards, and auto-scaling triggers.Stored in a lightweight time-series database for 93 days.
LogsStructured or unstructured text events and activity records rich with metadata (e.g., Syslog, Windows Event Logs, HTTP Access Logs, Diagnostic Traces).Deep root-cause analysis, trend auditing, complex security queries, and compliance reporting.Stored in Log Analytics Workspaces queryable via KQL (Kusto Query Language); retainable up to 7 years.

2. Telemetry Data Sources

Azure Monitor automatically or via agents collects data from five distinct layers:

  1. Core Platform & Subscription Data: Azure Activity Logs, Entra ID Sign-In logs, Resource Health status, and service-level platform metrics (collected automatically without setup).
  2. Resource Diagnostics: Specific resource logs (e.g., Key Vault secret access, SQL Database execution logs, Network Security Group flow logs) streamed via Diagnostic Settings.
  3. Guest Operating Systems (IaaS): Performance counters, OS event logs, and Syslog collected from VMs (in Azure, AWS, GCP, or on-premises) via the Azure Monitor Agent (AMA).
  4. Application Code: Application performance monitoring (APM) telemetry collected via the Application Insights SDK or OpenTelemetry.
  5. Network & Custom Sources: Synthetic ping tests, network hop metrics, and custom REST API log ingestions.

3. Key Features & Capabilities

Application Insights (APM)

A feature of Azure Monitor designed for web developers and DevOps teams.

  • Request & Dependency Tracking: Tracks incoming HTTP request performance and downstream dependency calls (SQL, Redis, HTTP APIs).
  • Distributed Tracing: Maps requests end-to-end as they pass through microservices.
  • Exceptions & Telemetry: Captures unhandled stack traces and live application log output.
Log Analytics & KQL

The querying engine powering Azure Monitor.

  • Runs Kusto Query Language (KQL) to query millions of log records in seconds.
  • Allows joining data across different services, building custom charts, and feeding metrics directly into alerting rules.
Action Groups & Automated Alerts
  • Metric Alerts: Triggers immediately when numerical thresholds are crossed (e.g., CPU > 90% for 5 minutes).
  • Log Search Alerts: Executes scheduled KQL queries and alerts when matching event records exceed a set threshold.
  • Action Groups: Coordinates responses when alerts fire—sending emails/SMS, calling Webhooks, triggering Azure Logic Apps/Functions, or opening tickets in ITSM tools (ServiceNow).
Insights & Workbooks
  • Insights: Pre-packaged monitoring experiences tailored to specific services (e.g., VM Insights, Container Insights for AKS, Network Insights, Key Vault Insights).
  • Workbooks: Interactive, customizable reporting dashboards combining text, live KQL query results, and metrics charts.

Foundational CSPM vs Defender CSPM

While Foundational CSPM focuses on static misconfigurations, Defender CSPM provides context-aware risk analysis across identity, network, code, and data.

Detailed Feature Matrix

Capability CategoryFoundational CSPM (Free)Defender CSPM (Paid)
Pricing Model$0 (Free for Azure, AWS, GCP)Paid (Billed per resource/month)
Microsoft Cloud Security Benchmark (MCSB)IncludedIncluded
Classic Secure ScoreIncludedIncluded
Asset Discovery & InventoryIncludedIncluded
Custom RecommendationsBasic Azure PolicyAdvanced Graph Query Custom Policies
Agentless VM Vulnerability ScanningNot IncludedIncluded (OS disk analysis without agents)
Attack Path AnalysisNot IncludedIncluded (Context-aware exploit paths)
Cloud Security Graph & ExplorerNot IncludedIncluded (Graph queries across identities & data)
Data-Aware Security Posture (DSPM)Not IncludedIncluded (Automated sensitive data discovery)
Code-to-Cloud GovernanceBasic DevOps visibilityIncluded (PR annotations & IaC risk mapping)
Governance Rules & Task AssignmentNot IncludedIncluded (Assign owners & set SLA deadlines)
Regulatory Compliance FrameworksBasic MCSB MappingFull (PCI-DSS, ISO 27001, NIST 800-53, HIPAA)

Breakdown of Key Features in Defender CSPM

1. Attack Path Analysis

Instead of giving you a disconnected list of 500 alerts, Defender CSPM maps the relationships between resources to reveal multi-step exploit vectors.

  • Example: It alerts you if an Internet-facing VM with an unpatched Remote Code Execution (RCE) vulnerability holds a Managed Identity that has read access to a Storage Account containing PII.
2. Agentless Disk & Container Scanning

Scans virtual machine disks (OS and data disks) and container images at rest using Azure storage snapshots. It identifies OS vulnerabilities, installed software flaws (CVEs), and exposed secrets without installing agents or consuming CPU cycles on the target VMs.

3. Cloud Security Graph & Cloud Security Explorer

Constructs a graph database representing your multicloud architecture. Security engineers can run custom KQL-like queries through the Cloud Security Explorer:

  • Query Example: Find all internet-exposed VMs running Linux that have active critical vulnerabilities AND can write to Key Vaults.
4. Data-Aware Security Posture Management (DSPM)

Automatically scans Blob Storage and database instances for sensitive data types (credit card numbers, national identification numbers, private keys). It combines data sensitivity with network exposure to prioritize security fixes on storage accounts hosting high-risk data.

5. Automated Governance Rules

Allows security administrators to enforce SLA-driven remediation targets across engineering teams:

  • Automatically assign High Severity recommendations to specific subscription owners via email with a mandatory 30-day remediation SLA.