Containers

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

graph LR subgraph Entry["Traffic and Entry"] ING[Ingress and WAF] end subgraph ControlPlane["AKS Control Plane"] API[API Server] ETCD[etcd Encrypted] end subgraph Nodes["Node Pools"] SYS[System Nodes] USER[User Nodes] end subgraph Workloads["Workloads"] PODS[Application Pods] end subgraph Security["Security Controls"] ACR[Container Registry] KV[Key Vault CSI] DFC[Defender for Containers] POL[Azure Policy] end API --> ETCD API --> SYS API --> USER ING --> PODS SYS --> PODS USER --> PODS ACR -->|Image Pull| SYS ACR -->|Image Pull| USER KV -.->|Secrets| PODS DFC -.->|Runtime Signals| SYS DFC -.->|Runtime Signals| USER POL -.->|Admission and Guardrails| API style Entry stroke:#ec4899,stroke-width:2px style ControlPlane stroke:#4ade80,stroke-width:2px style Nodes stroke:#22d3ee,stroke-width:2px style Workloads stroke:#a855f7,stroke-width:2px style Security stroke:#f59e0b,stroke-width:2px

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.

PropertyValue
Defender PlanDefender for Containers (runtime + VA + admission)
Azure Policy Built-inYes — AKS initiative (50+ policies via Gatekeeper)
Diagnostic Logskube-apiserver, kube-audit, kube-controller-manager, guard
Recommended ConfigPrivate cluster, Entra ID RBAC, Azure CNI Overlay, OIDC issuer

Baseline Controls

NS✓ Supported

Private Cluster

○ ManualCustomer
NS✓ Supported

Network Policies

○ ManualCustomer
IM✓ Supported

Entra ID RBAC for K8s

○ ManualCustomer
IM✓ Supported

Workload Identity

○ ManualCustomer
DP✓ Supported

etcd Encryption

● DefaultMicrosoft
DP✓ Supported

Key Vault CSI Driver

○ ManualCustomer
PV✓ Supported

Azure Policy (Gatekeeper)

○ ManualCustomer
PV✓ Supported

Node Auto-Upgrade

○ ManualCustomer
LT✓ Supported

Defender for Containers

○ ManualCustomer
ES✓ Supported

MDE on Nodes

○ ManualCustomer

Terraform: Hardened AKS Cluster

hcl
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
  1. When creating a new AKS cluster, on the Networking tab, check "Enable private cluster"
  2. For existing clusters, navigate to your AKS resource in the Azure Portal
  3. In the left menu under Settings, select Networking
  4. Under API server access, enable "Private cluster" (note: this may cause a brief cluster restart)
  5. Under Network policy, select Azure or Calico to enable pod-to-pod segmentation
  6. Under Authorized IP ranges, add specific IPs allowed to reach the API server (if not fully private)
NS-2 — Private Link (API Server)
  1. Navigate to your AKS resource in the Azure Portal
  2. In the left menu under Settings, select Networking
  3. Under API server access, verify that "Private cluster" is enabled
  4. The API server is automatically placed behind Private Link when private cluster mode is on
  5. Access kubectl from a VM in the same or peered VNet, or via Azure Bastion
  6. Configure private DNS zone for API server name resolution from on-premises networks
IM-3 — Workload Identity
  1. Navigate to your AKS resource in the Azure Portal
  2. In the left menu under Settings, select Cluster configuration
  3. Under Security, enable "OIDC Issuer"
  4. Enable "Workload Identity" (this requires OIDC Issuer to be enabled)
  5. Click Apply
  6. After enabling, create a Kubernetes ServiceAccount annotated with the client ID of your Entra ID managed identity
  7. Configure federated credential on the managed identity in Entra ID
DP-3 — Data in Transit Encryption
  1. Navigate to your AKS resource in the Azure Portal
  2. In the left menu under Settings, select Service mesh (if available)
  3. Enable Istio-based service mesh for automatic mTLS between pods
  4. Alternatively, for ingress TLS, deploy NGINX Ingress Controller or use Application Gateway
  5. Configure TLS termination on the ingress controller with certificates from Key Vault
  6. Note: Service mesh mTLS provides encryption for all pod-to-pod communication within the cluster
ES-1 — Defender for Containers
  1. Navigate to Microsoft Defender for Cloud in the Azure Portal
  2. In the left menu, select Environment settings
  3. Select your subscription
  4. Under Cloud Workload Protection (CWP), find the Containers row
  5. Toggle the plan to On
  6. Click Settings and ensure "Runtime protection" and "Agentless scanning" are enabled
  7. Click Save — the Defender agent will be auto-deployed to AKS clusters
PV-5 — Image Vulnerability Scanning
  1. Enable Defender for Containers (see ES-1 steps above) — this automatically enables image vulnerability scanning in ACR
  2. Navigate to your Container Registry resource in the Azure Portal
  3. In the left menu, select Microsoft Defender for Cloud to view vulnerability findings
  4. To enforce policies, navigate to your AKS resource > Policies
  5. Assign the "Kubernetes cluster containers should only use allowed images" policy
  6. Configure the allowed image registries regex pattern
  7. Apply the policy — images from untrusted registries will be blocked by admission control
DS-6 — Supply Chain Security
  1. Navigate to your AKS resource in the Azure Portal
  2. In the left menu under Settings, select Policies
  3. Assign the "Kubernetes cluster containers should only use allowed images" policy
  4. Configure allowed registry patterns to restrict to your trusted ACR instances
  5. For image signing verification, navigate to your Container Registry
  6. Enable Content Trust under Settings > Content Trust (if available)
  7. 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

NS✓ Supported

Private Endpoint

○ ManualCustomer
NS✓ Supported

Disable Public Access

○ ManualCustomer
PV✓ Supported

Content Trust

○ ManualCustomer
PV✓ Supported

Quarantine Policy

○ ManualCustomer

Azure Portal Instructions

ACR Baseline Configuration Workflow
  1. Navigate to your Azure Container Registry resource in the Azure Portal.
  2. Use Premium SKU if you need private endpoints, geo-replication, or advanced security controls.
  3. Under Networking, disable public network access and create a private endpoint for the registry.
  4. Under Policies, enable Content trust and Quarantine where your image workflow supports signed and approved artifacts.
  5. Integrate the registry with Defender for Containers so vulnerability findings are surfaced centrally.
  6. Validate that AKS or other clients pull from the private registry path rather than a public endpoint.
hcl
# 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

Use Premium SKU for private endpoints and geo-replication. Enable content trust for image signing and quarantine policies to block unscanned images from being pulled.

AKS Diagnostic Settings

hcl
# 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

PolicyEffectPurpose
No privileged containersDenyPrevent container breakout via privileged mode
Containers must not run as rootDenyEnforce non-root UID in containers
Read-only root filesystemAuditDetect containers with writable root FS
Allowed container imagesDenyOnly pull from approved registries (ACR)
No host namespacesDenyBlock hostPID, hostIPC, hostNetwork
Resource limits requiredDenyPrevent resource exhaustion / noisy neighbor
Drop all capabilitiesDenyEnforce minimal Linux capabilities
No LoadBalancer servicesAuditForce traffic through ingress controller

Image Supply Chain

Never pull images directly from Docker Hub or public registries in production. Import approved images into your private ACR, scan them with Defender for Containers, enable content trust for image signing, and use quarantine policies to block unscanned images from being deployed.