Containers & Kubernetes Security Baselines
AKS is a complex surface — it combines infrastructure (nodes), orchestration (Kubernetes API), and application (container images) security. The baseline must cover all three layers: harden the cluster control plane, secure the node pools, enforce admission policies, and scan every container image.
AKS Security Layers
Azure Kubernetes Service (AKS)
Managed Kubernetes with integrated CI/CD, monitoring, and security. AKS has the most complex security baseline of any Azure service because it spans infrastructure, platform, and application layers.
| Property | Value |
|---|---|
| Defender Plan | Defender for Containers (runtime + VA + admission) |
| Azure Policy Built-in | Yes — AKS initiative (50+ policies via Gatekeeper) |
| Diagnostic Logs | kube-apiserver, kube-audit, kube-controller-manager, guard |
| Recommended Config | Private cluster, Entra ID RBAC, Azure CNI Overlay, OIDC issuer |
Baseline Controls
| Domain | Feature | Supported | Default | Responsibility |
|---|---|---|---|---|
| NS | Private Cluster | ✓ | Manual | Customer |
| NS | Network Policies | ✓ | Manual | Customer |
| IM | Entra ID RBAC for K8s | ✓ | Manual | Customer |
| IM | Workload Identity | ✓ | Manual | Customer |
| DP | etcd Encryption | ✓ | Default | Microsoft |
| DP | Key Vault CSI Driver | ✓ | Manual | Customer |
| PV | Azure Policy (Gatekeeper) | ✓ | Manual | Customer |
| PV | Node Auto-Upgrade | ✓ | Manual | Customer |
| LT | Defender for Containers | ✓ | Manual | Customer |
| ES | MDE on Nodes | ✓ | Manual | Customer |
Private Cluster
Network Policies
Entra ID RBAC for K8s
Workload Identity
etcd Encryption
Key Vault CSI Driver
Azure Policy (Gatekeeper)
Node Auto-Upgrade
Defender for Containers
MDE on Nodes
Terraform: Hardened AKS Cluster
resource "azurerm_kubernetes_cluster" "main" {
name = "aks-prod-001"
resource_group_name = azurerm_resource_group.aks.name
location = azurerm_resource_group.aks.location
dns_prefix = "aks-prod"
kubernetes_version = "1.29"
# NS: Private cluster — API server not exposed to internet
private_cluster_enabled = true
# IM-1: Entra ID RBAC (disable local accounts)
local_account_disabled = true
azure_active_directory_role_based_access_control {
azure_rbac_enabled = true
managed = true
admin_group_object_ids = [azuread_group.aks_admins.object_id]
}
# IM-3: Workload Identity for pod-level Entra auth
oidc_issuer_enabled = true
workload_identity_enabled = true
# NS: Azure CNI Overlay for network policies
network_profile {
network_plugin = "azure"
network_plugin_mode = "overlay"
network_policy = "calico" # Kubernetes network policies
service_cidr = "172.16.0.0/16"
dns_service_ip = "172.16.0.10"
}
# System node pool
default_node_pool {
name = "system"
vm_size = "Standard_D4s_v5"
node_count = 3
vnet_subnet_id = azurerm_subnet.aks_nodes.id
zones = ["1", "2", "3"]
os_sku = "AzureLinux" # Minimal attack surface
temporary_name_for_rotation = "systemtemp"
upgrade_settings {
max_surge = "33%"
}
}
# PV: Auto-upgrade channel
automatic_channel_upgrade = "stable"
node_os_channel_upgrade = "NodeImage"
# DP: Secrets Store CSI Driver for Key Vault
key_vault_secrets_provider {
secret_rotation_enabled = true
secret_rotation_interval = "2m"
}
# PV: Azure Policy add-on (Gatekeeper)
azure_policy_enabled = true
# LT: Defender profile
microsoft_defender {
log_analytics_workspace_id = azurerm_log_analytics_workspace.sentinel.id
}
# LT: OMS Agent for Container Insights
oms_agent {
log_analytics_workspace_id = azurerm_log_analytics_workspace.sentinel.id
}
identity {
type = "SystemAssigned"
}
maintenance_window_auto_upgrade {
frequency = "Weekly"
interval = 1
day_of_week = "Sunday"
start_time = "02:00"
duration = 4
}
}
# ACR — Azure Container Registry
resource "azurerm_kubernetes_cluster" "main" {
name = "aks-prod-001"
resource_group_name = azurerm_resource_group.aks.name
location = azurerm_resource_group.aks.location
dns_prefix = "aks-prod"
kubernetes_version = "1.29"
# NS: Private cluster — API server not exposed to internet
private_cluster_enabled = true
# IM-1: Entra ID RBAC (disable local accounts)
local_account_disabled = true
azure_active_directory_role_based_access_control {
azure_rbac_enabled = true
managed = true
admin_group_object_ids = [azuread_group.aks_admins.object_id]
}
# IM-3: Workload Identity for pod-level Entra auth
oidc_issuer_enabled = true
workload_identity_enabled = true
# NS: Azure CNI Overlay for network policies
network_profile {
network_plugin = "azure"
network_plugin_mode = "overlay"
network_policy = "calico" # Kubernetes network policies
service_cidr = "172.16.0.0/16"
dns_service_ip = "172.16.0.10"
}
# System node pool
default_node_pool {
name = "system"
vm_size = "Standard_D4s_v5"
node_count = 3
vnet_subnet_id = azurerm_subnet.aks_nodes.id
zones = ["1", "2", "3"]
os_sku = "AzureLinux" # Minimal attack surface
temporary_name_for_rotation = "systemtemp"
upgrade_settings {
max_surge = "33%"
}
}
# PV: Auto-upgrade channel
automatic_channel_upgrade = "stable"
node_os_channel_upgrade = "NodeImage"
# DP: Secrets Store CSI Driver for Key Vault
key_vault_secrets_provider {
secret_rotation_enabled = true
secret_rotation_interval = "2m"
}
# PV: Azure Policy add-on (Gatekeeper)
azure_policy_enabled = true
# LT: Defender profile
microsoft_defender {
log_analytics_workspace_id = azurerm_log_analytics_workspace.sentinel.id
}
# LT: OMS Agent for Container Insights
oms_agent {
log_analytics_workspace_id = azurerm_log_analytics_workspace.sentinel.id
}
identity {
type = "SystemAssigned"
}
maintenance_window_auto_upgrade {
frequency = "Weekly"
interval = 1
day_of_week = "Sunday"
start_time = "02:00"
duration = 4
}
}
# ACR — Azure Container Registry
Azure Portal Instructions
Step-by-step instructions for configuring each AKS control through the Azure Portal.
NS-1 — Private Cluster
- When creating a new AKS cluster, on the Networking tab, check "Enable private cluster"
- For existing clusters, navigate to your AKS resource in the Azure Portal
- In the left menu under Settings, select Networking
- Under API server access, enable "Private cluster" (note: this may cause a brief cluster restart)
- Under Network policy, select Azure or Calico to enable pod-to-pod segmentation
- Under Authorized IP ranges, add specific IPs allowed to reach the API server (if not fully private)
NS-2 — Private Link (API Server)
- Navigate to your AKS resource in the Azure Portal
- In the left menu under Settings, select Networking
- Under API server access, verify that "Private cluster" is enabled
- The API server is automatically placed behind Private Link when private cluster mode is on
- Access kubectl from a VM in the same or peered VNet, or via Azure Bastion
- Configure private DNS zone for API server name resolution from on-premises networks
IM-3 — Workload Identity
- Navigate to your AKS resource in the Azure Portal
- In the left menu under Settings, select Cluster configuration
- Under Security, enable "OIDC Issuer"
- Enable "Workload Identity" (this requires OIDC Issuer to be enabled)
- Click Apply
- After enabling, create a Kubernetes ServiceAccount annotated with the client ID of your Entra ID managed identity
- Configure federated credential on the managed identity in Entra ID
DP-3 — Data in Transit Encryption
- Navigate to your AKS resource in the Azure Portal
- In the left menu under Settings, select Service mesh (if available)
- Enable Istio-based service mesh for automatic mTLS between pods
- Alternatively, for ingress TLS, deploy NGINX Ingress Controller or use Application Gateway
- Configure TLS termination on the ingress controller with certificates from Key Vault
- Note: Service mesh mTLS provides encryption for all pod-to-pod communication within the cluster
ES-1 — Defender for Containers
- Navigate to Microsoft Defender for Cloud in the Azure Portal
- In the left menu, select Environment settings
- Select your subscription
- Under Cloud Workload Protection (CWP), find the Containers row
- Toggle the plan to On
- Click Settings and ensure "Runtime protection" and "Agentless scanning" are enabled
- Click Save — the Defender agent will be auto-deployed to AKS clusters
PV-5 — Image Vulnerability Scanning
- Enable Defender for Containers (see ES-1 steps above) — this automatically enables image vulnerability scanning in ACR
- Navigate to your Container Registry resource in the Azure Portal
- In the left menu, select Microsoft Defender for Cloud to view vulnerability findings
- To enforce policies, navigate to your AKS resource > Policies
- Assign the "Kubernetes cluster containers should only use allowed images" policy
- Configure the allowed image registries regex pattern
- Apply the policy — images from untrusted registries will be blocked by admission control
DS-6 — Supply Chain Security
- Navigate to your AKS resource in the Azure Portal
- In the left menu under Settings, select Policies
- Assign the "Kubernetes cluster containers should only use allowed images" policy
- Configure allowed registry patterns to restrict to your trusted ACR instances
- For image signing verification, navigate to your Container Registry
- Enable Content Trust under Settings > Content Trust (if available)
- Note: Full Notary v2 / image integrity configuration requires Azure CLI —
az aks update --enable-image-integrity
Azure Container Registry (ACR)
ACR stores and scans container images used by AKS. Premium SKU is required for private endpoints, geo-replication, and content trust (image signing).
Baseline Controls
| Domain | Feature | Supported | Default | Responsibility |
|---|---|---|---|---|
| NS | Private Endpoint | ✓ | Manual | Customer |
| NS | Disable Public Access | ✓ | Manual | Customer |
| PV | Content Trust | ✓ | Manual | Customer |
| PV | Quarantine Policy | ✓ | Manual | Customer |
Private Endpoint
Disable Public Access
Content Trust
Quarantine Policy
Azure Portal Instructions
ACR Baseline Configuration Workflow
- Navigate to your Azure Container Registry resource in the Azure Portal.
- Use Premium SKU if you need private endpoints, geo-replication, or advanced security controls.
- Under Networking, disable public network access and create a private endpoint for the registry.
- Under Policies, enable Content trust and Quarantine where your image workflow supports signed and approved artifacts.
- Integrate the registry with Defender for Containers so vulnerability findings are surfaced centrally.
- Validate that AKS or other clients pull from the private registry path rather than a public endpoint.
# ACR with private endpoint and image scanning
resource "azurerm_container_registry" "main" {
name = "acrprod001"
resource_group_name = azurerm_resource_group.aks.name
location = azurerm_resource_group.aks.location
sku = "Premium" # Required for PE and geo-replication
# NS: Disable public access
public_network_access_enabled = false
# PV: Content trust for signed images
trust_policy_enabled = true
# PV: Quarantine policy for unscanned images
quarantine_policy_enabled = true
}
# NS: Private endpoint for ACR
resource "azurerm_private_endpoint" "acr" {
name = "pe-acr-prod"
location = azurerm_resource_group.aks.location
resource_group_name = azurerm_resource_group.aks.name
subnet_id = azurerm_subnet.private_endpoints.id
private_service_connection {
name = "psc-acr-prod"
private_connection_resource_id = azurerm_container_registry.main.id
subresource_names = ["registry"]
is_manual_connection = false
}
}# ACR with private endpoint and image scanning
resource "azurerm_container_registry" "main" {
name = "acrprod001"
resource_group_name = azurerm_resource_group.aks.name
location = azurerm_resource_group.aks.location
sku = "Premium" # Required for PE and geo-replication
# NS: Disable public access
public_network_access_enabled = false
# PV: Content trust for signed images
trust_policy_enabled = true
# PV: Quarantine policy for unscanned images
quarantine_policy_enabled = true
}
# NS: Private endpoint for ACR
resource "azurerm_private_endpoint" "acr" {
name = "pe-acr-prod"
location = azurerm_resource_group.aks.location
resource_group_name = azurerm_resource_group.aks.name
subnet_id = azurerm_subnet.private_endpoints.id
private_service_connection {
name = "psc-acr-prod"
private_connection_resource_id = azurerm_container_registry.main.id
subresource_names = ["registry"]
is_manual_connection = false
}
}ACR Security Essentials
AKS Diagnostic Settings
# LT-3: AKS Diagnostic Settings
resource "azurerm_monitor_diagnostic_setting" "aks" {
name = "diag-aks-prod"
target_resource_id = azurerm_kubernetes_cluster.main.id
log_analytics_workspace_id = azurerm_log_analytics_workspace.sentinel.id
enabled_log { category = "kube-apiserver" }
enabled_log { category = "kube-audit-admin" }
enabled_log { category = "kube-controller-manager" }
enabled_log { category = "guard" }
metric { category = "AllMetrics" }
}# LT-3: AKS Diagnostic Settings
resource "azurerm_monitor_diagnostic_setting" "aks" {
name = "diag-aks-prod"
target_resource_id = azurerm_kubernetes_cluster.main.id
log_analytics_workspace_id = azurerm_log_analytics_workspace.sentinel.id
enabled_log { category = "kube-apiserver" }
enabled_log { category = "kube-audit-admin" }
enabled_log { category = "kube-controller-manager" }
enabled_log { category = "guard" }
metric { category = "AllMetrics" }
}Essential Azure Policy Assignments for AKS
| Policy | Effect | Purpose |
|---|---|---|
| No privileged containers | Deny | Prevent container breakout via privileged mode |
| Containers must not run as root | Deny | Enforce non-root UID in containers |
| Read-only root filesystem | Audit | Detect containers with writable root FS |
| Allowed container images | Deny | Only pull from approved registries (ACR) |
| No host namespaces | Deny | Block hostPID, hostIPC, hostNetwork |
| Resource limits required | Deny | Prevent resource exhaustion / noisy neighbor |
| Drop all capabilities | Deny | Enforce minimal Linux capabilities |
| No LoadBalancer services | Audit | Force traffic through ingress controller |
Image Supply Chain