Enforce AKS Compliance with Policy
Use Terraform and Azure Policy to block privileged AKS pods.
Introduction
30 Second Summary
An application can look healthy while quietly asking for more control than it needs. In a regulated environment, teams need proof that unsafe software is stopped before it starts.
In this project, you will enforce a Linux pod security baseline on an existing Azure Kubernetes Service (AKS) Standard cluster. You will use Azure Policy with Terraform to turn a privileged pod from an accepted deployment into a denied request while a compliant pod reaches Running.
What You'll Build
Your finished demo shows the same privileged manifest move from accepted to blocked while a compliant pod reaches Running.
By the end of this project, you'll have:
- Reusable policy as code that turns a reviewed Terraform plan into the same security baseline every time.
- A before-and-after test proving admission control changes the same privileged pod from created to denied through Open Policy Agent Gatekeeper.
- A compliance evidence set where nginx-unprivileged reaches 1/1 readiness with Running status in Windows PowerShell.
- Secret Mission: Add a remediation-focused non-compliance message to the Terraform-managed assignment.
Are there any prerequisites?
You need an active Azure subscription with an existing disposable AKS Standard cluster that has a Linux node pool.
You also need permissions equivalent to Resource Policy Contributor or Owner on a cluster where no existing control denies the privileged test pod.
Before We Start
This short checkpoint locks in the disposable Azure Kubernetes Service (AKS) cluster you will govern. It also captures why privileged containers pose a risk before you make any governance changes.
Verify Your Toolchain and AKS Target
Governance changes become risky when a command targets the wrong Azure Kubernetes Service (AKS) cluster. A stale subscription or Kubernetes context can move the change outside your disposable lab.
This step gives you a verified Windows PowerShell workspace for the intended Microsoft Azure subscription.
You'll check Terraform, Azure CLI, and kubectl before connecting to the lab cluster.
In this step, get ready to:
- Create the aks-policy-governance workspace in Windows PowerShell.
- Verify the required command-line tool versions.
- Connect kubectl to the intended AKS cluster.
Create the workspace and check tool versions
A dedicated workspace keeps the Terraform files from later steps together. Start with a clean PowerShell location on your Desktop.
- Press the Windows key to open the search bar.
- Type Windows PowerShell into the search bar.
- Press Enter to open Windows PowerShell.
- Move to your Desktop by running this command:
Set-Location -Path ~/Desktop -PassThru
What does this command do?
PowerShell changes its working location to your Desktop. -PassThru prints the selected path so you can confirm where the project folder will be created.
- Create the aks-policy-governance folder by running these commands:
New-Item -Path . -Name "aks-policy-governance" -ItemType "Directory" -Force
Set-Location -Path .\aks-policy-governance -PassThru
What do these commands do?
- New-Item creates the project folder on your Desktop.
- -Force safely returns the existing folder when you already created it.
- Set-Location moves PowerShell into that folder.
You should see a path ending in Desktop\aks-policy-governance.
- Check the three command-line tools by running:
terraform -version
az --version
kubectl version --client
What should the version check prove?
- terraform -version must report Terraform 1.16.0 or later.
- az --version must report Azure CLI 2.47.0 or later.
- kubectl version --client must print a client version. You will compare its minor version with the AKS control plane later in this step.
✔️ All three tools meet the requirements
Your local commands are available. Terraform meets the required minimum.
Azure CLI also meets its minimum. Keep this PowerShell window open for the cluster checks.
ⓧ I see an older version
Update each tool that falls below its requirement. Return to the version check after each update.
- Download Terraform 1.16.0 for Windows from the official Terraform install page.
- Choose the Windows binary that matches your computer's architecture.
- Extract terraform.exe into the folder that currently provides Terraform on your system path.
- Upgrade Azure CLI by running this command:
az upgrade
What does this command do?
Azure CLI checks for its latest supported release. It also updates installed Azure CLI extensions by default.
Azure CLI upgrade unavailable?
Azure CLI releases before 2.11.0 do not support the in-tool upgrade command. Use the official Windows installation guide to reinstall the current release.
Need help? Help me update Azure CLI on Windows
- Leave the current kubectl client in place until you compare it with the AKS control-plane version later in this step.
- Open a new Windows PowerShell session after completing the updates.
- Return to the aks-policy-governance folder on your Desktop.
- Repeat the three version checks shown above.
ⓧ One or more commands are not found
Install each missing tool before connecting to Azure. A new PowerShell session picks up the updated system path.
- Download Terraform 1.16.0 for Windows from the official Terraform install page.
- Choose the Windows binary that matches your computer's architecture.
- Extract terraform.exe into a folder on your system path.
- Install Azure CLI through WinGet by running:
winget install --exact --id Microsoft.AzureCLI
What does this command do?
WinGet selects the official Azure CLI package by its exact package ID. It installs the latest available release.
Azure CLI installation failed?
Confirm that WinGet is available on your Windows installation. You can also use the MSI option on the official Azure CLI installation page.
Need help? Help me install Azure CLI with WinGet
- Install kubectl through WinGet by running:
winget install -e --id Kubernetes.kubectl
What does this command do?
WinGet installs the Kubernetes command-line client from the package identified as Kubernetes.kubectl.
kubectl installation failed?
Use the official kubectl Windows guide if WinGet cannot install the package.
Need help? Help me install kubectl on Windows
- Open a new Windows PowerShell session after the installations finish.
- Return to the aks-policy-governance folder on your Desktop.
- Repeat the three version checks shown above.
Good. Your local toolchain can now support the policy assignment workflow.
Sign in and select the target cluster
Azure CLI commands use the active subscription in your local session. Confirming that subscription prevents the policy work from reaching another environment.
The sign-in flow uses browser authentication. Your password stays out of the PowerShell command history.
- Start interactive Azure authentication by running:
az login
What does this command do?
Azure CLI opens an interactive sign-in flow. The completed flow connects your local CLI session to your Azure account.
- Complete the authentication flow in your browser.
- Return to Windows PowerShell after authentication succeeds.
Authentication did not complete?
Check that the browser flow uses the Azure account with access to the lab subscription. Corporate sign-in policies may require an additional approval step.
Need help? Help me troubleshoot az login
- Display the active Azure subscription by running:
az account show
What does this command do?
Azure CLI displays the subscription currently used for later commands. The output includes its name and ID.
- Confirm the visible subscription name matches your intended lab subscription.
- Confirm the visible subscription ID matches your intended lab subscription.
- Stop here if either value belongs to another Azure environment.
Finding the correct resource names can be fiddly. Use the existing AKS cluster details you identified before this step.
- Store the target resource group and cluster names in PowerShell by running:
$RESOURCE_GROUP = "[[RESOURCE_GROUP_NAME="your existing AKS resource group name"]]"
$CLUSTER_NAME = "[[CLUSTER_NAME="your existing AKS cluster name"]]"
What do these variables hold?
- $RESOURCE_GROUP holds the name of the resource group containing the existing AKS cluster.
- $CLUSTER_NAME holds the existing AKS cluster name.
- Both values remain available for commands in this PowerShell session.
Protect the right cluster
Confirm that $CLUSTER_NAME identifies the disposable AKS Standard lab cluster from earlier. Stop if the cluster is shared or production-facing.
Confirm that no existing policy or admission rule is configured to deny the privileged test pod. The next step depends on observing the cluster's current behavior.
- Configure kubectl credentials for the selected cluster by running:
az aks get-credentials --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME
What does this command do?
Azure CLI retrieves credentials for the named AKS cluster. It saves the cluster connection in your local Kubernetes configuration.
- Check any replacement prompt against the cluster name before confirming it.
- Confirm the command completes without an authentication or permission error.
Could not retrieve cluster credentials?
- Check that $RESOURCE_GROUP contains the existing AKS resource group name.
- Check that $CLUSTER_NAME contains the existing AKS cluster name.
- Confirm the active Azure subscription still matches the lab subscription.
Need help? Help me troubleshoot AKS credentials
Verify the Kubernetes context and nodes
The final check proves that the local client can reach the selected control plane. It also confirms that every AKS node is healthy before governance changes begin.
- Compare the AKS control-plane version with your local client by running:
az aks show --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --query kubernetesVersion --output tsv
kubectl version --client
How do these versions compare?
The first line returns the cluster's Kubernetes version. The second command prints the local client version.
Compare their major and minor numbers. The kubectl client must remain within one minor version of the control plane.
✔️ The versions are within one minor
Your local client is compatible with the AKS control plane. Continue to the connectivity check.
ⓧ The versions are more than one minor apart
Install a compatible kubectl release before continuing. Its minor version can be one above or one below the AKS control plane.
- Open the official kubectl Windows installation guide.
- Choose a Windows release that falls within the supported minor-version range.
- Install the selected client using one of the documented Windows methods.
- Return to the aks-policy-governance PowerShell session.
- Repeat the version comparison before continuing.
Before you check the connection, do you expect every node to report the same healthy status?
- Verify the control-plane connection and node health by running:
kubectl cluster-info
kubectl get nodes
What do these commands prove?
- kubectl cluster-info proves that your local context can reach the selected Kubernetes control plane.
- kubectl get nodes returns the node table for that context.
- The node table provides the final health check before you test admission behavior.
You should see the Kubernetes control-plane URL. Every listed node should show Ready under STATUS.
Cluster unreachable or nodes not Ready?
- Repeat the credential command after confirming the two PowerShell variables.
- Confirm the AKS cluster is running in the intended Azure subscription.
- Stop the project if any node remains outside the Ready state.
Need help? Help me diagnose my AKS connection
- Capture a Windows PowerShell screenshot showing both final commands.
- Include the complete control-plane result in the screenshot.
- Include the complete node table in the screenshot.
- Replace cluster names and node names with labeled placeholders before sharing the image.
- Replace URLs and timestamps with labeled placeholders before sharing the image.
That's the target lock complete. Your verified tools now point at the disposable AKS cluster you intend to govern.
Next, you'll expose the current policy gap by testing whether that cluster admits a privileged pod.
Prove the Privileged Pod Risk
Your verified toolchain now points at the intended AKS cluster. You can safely capture a baseline from the disposable lab target.
The intended enterprise safeguard has not been added yet. This step submits a Pod with securityContext.privileged: true to reveal the current Kubernetes admission behavior.
In this step, get ready to:
- Create two test manifests with one privileged security difference.
- Capture the cluster's current response to the privileged workload.
- Delete the temporary privileged Pod after collecting the evidence.
Create the test manifests
A YAML manifest describes the state Kubernetes should create. These two manifests use the same container image.
The privileged manifest adds one security setting. The control manifest leaves that setting out.
- Press the Windows key to open the Windows search bar.
- Type Notepad into the search field.
- Press Enter to start Notepad.
- Create nginx-privileged.yaml by pasting this manifest into the blank document:
apiVersion: v1
kind: Pod
metadata:
name: nginx-privileged
spec:
containers:
- name: nginx-privileged
image: mcr.microsoft.com/oss/nginx/nginx:1.15.5-alpine
securityContext:
privileged: true
What Does This Manifest Define?
- The metadata section gives the Pod its nginx-privileged name.
- The container uses the mcr.microsoft.com/oss/nginx/nginx:1.15.5-alpine image.
- The privileged: true setting requests privileged access for the container.
- Save the document as nginx-privileged.yaml inside the aks-policy-governance workspace.
- Check that the Notepad tab displays nginx-privileged.yaml as the saved filename.
Did Notepad Add a Text Extension?
Check the saved filename for an extra .txt extension. Rename the file so its complete name is nginx-privileged.yaml.
Still stuck? Help me save a YAML file in Notepad without adding a .txt extension
✔️ Awesome, I've got everything!
Great. Confirm that nginx-privileged.yaml is saved in the aks-policy-governance workspace.
ⓧ I'd like to double check the full code
apiVersion: v1
kind: Pod
metadata:
name: nginx-privileged
spec:
containers:
- name: nginx-privileged
image: mcr.microsoft.com/oss/nginx/nginx:1.15.5-alpine
securityContext:
privileged: true
What Should Match?
Compare every line with your saved file. The indentation beneath containers must match this reference.
The second manifest provides the compliant control workload. Its behavior later proves that enforcement can preserve an allowed deployment.
- Start a second blank document in Notepad.
- Create nginx-unprivileged.yaml by pasting this manifest into the blank document:
apiVersion: v1
kind: Pod
metadata:
name: nginx-unprivileged
spec:
containers:
- name: nginx-unprivileged
image: mcr.microsoft.com/oss/nginx/nginx:1.15.5-alpine
What Does the Control Manifest Do?
- The Pod uses the nginx-unprivileged name.
- The container uses the same NGINX image as the privileged test.
- The manifest defines no privileged security context.
- Save the document as nginx-unprivileged.yaml inside the aks-policy-governance workspace.
- Compare the two saved manifests in Notepad.
You should find one security difference. The privileged manifest contains securityContext with privileged: true.
Do the Manifests Look Too Similar?
Confirm that only nginx-privileged.yaml contains the securityContext section. Check that both files use the same container image.
Need another pair of eyes? Help me compare these privileged and unprivileged Kubernetes manifests
✔️ Awesome, I've got everything!
Both manifests are ready. Keep them in the aks-policy-governance workspace for the before-policy test.
ⓧ I'd like to double check the full code
apiVersion: v1
kind: Pod
metadata:
name: nginx-unprivileged
spec:
containers:
- name: nginx-unprivileged
image: mcr.microsoft.com/oss/nginx/nginx:1.15.5-alpine
What Should Match?
Compare every line with your saved file. This reference ends after the container image because the control workload has no privileged security context.
Submit the privileged Pod
Applying the manifest sends the requested Pod configuration to the Kubernetes API server. The response captures the cluster's admission decision before governance enforcement.
Before you run this, take a moment to predict whether the cluster accepts or rejects the privileged Pod.
- Switch back to Windows PowerShell from earlier.
- Submit the privileged manifest by running this command:
kubectl apply -f nginx-privileged.yaml
What Does This Command Do?
The kubectl apply command submits the desired Pod configuration. The -f flag points kubectl at nginx-privileged.yaml.
You should see pod/nginx-privileged created. The cluster has accepted the privileged workload.
Was the Privileged Pod Rejected?
Stop this project if the request is denied. A pre-existing admission control conflicts with the required before-policy baseline.
Check the current Kubernetes context before investigating further. Help me diagnose why my privileged Pod was rejected before Azure Policy was enabled
Before you check the Pod, predict whether its status has reached Running.
- Check the privileged Pod's status by running this command:
kubectl get pod nginx-privileged
What Does the Status Prove?
The command retrieves the current row for nginx-privileged. A Running status proves the admitted workload started on the cluster.
- Repeat the status command until the STATUS column shows Running.
You now have the crucial before-policy evidence. The lab cluster admitted the privileged Pod before the enterprise control was introduced.
Is the Pod Still Starting?
Allow the container a short time to start. Run the same status command again until the row reaches Running.
If the status stalls or reports a container problem, help me troubleshoot why nginx-privileged is not reaching Running
Delete the temporary Pod
The running Pod has produced the evidence you need. Removing it leaves the cluster clean while preserving both manifests for the controlled after-policy test.
- Delete the temporary privileged Pod by running this command:
kubectl delete -f nginx-privileged.yaml
What Does This Command Remove?
The command deletes the live Pod defined by nginx-privileged.yaml. The local manifest remains in your workspace.
You should see pod "nginx-privileged" deleted. This confirms that the temporary workload has been removed.
Was the Pod Already Missing?
Confirm that kubectl still targets the validated AKS cluster. Check that the filename is exactly nginx-privileged.yaml.
Still blocked? Help me troubleshoot deleting nginx-privileged from my current AKS context
Your baseline is complete. Next, you will add the policy enforcement engine that turns this visible risk into a governed admission decision.
Enable the Policy Enforcement Engine
Your validated Windows PowerShell session already targets the correct AKS cluster. The first test showed that this cluster admits a privileged pod without enterprise policy.
A Kubernetes manifest cannot create centralized Azure compliance reporting. This step installs the enforcement engine that connects cluster admissions to centralized policy.
Azure Policy distributes assignments to the cluster. Open Policy Agent Gatekeeper supplies admission control inside the cluster.
In this step, get ready to:
- Register the Azure provider required for Policy Insights.
- Enable the Azure Policy add-on on the validated AKS cluster.
- Verify the Azure Policy pods and Gatekeeper pods are running.
Prepare Azure for policy data
The Microsoft.PolicyInsights resource provider gives your subscription access to Azure Policy compliance operations. Registration must finish before the AKS add-on can use those operations.
This may take several minutes. The wait is Azure completing provider registration for the active subscription.
- Register Microsoft.PolicyInsights and wait for completion by running this command:
az provider register --namespace Microsoft.PolicyInsights --wait
Why wait for registration?
- The registration command enables the Policy Insights resource provider in your active Azure subscription.
- The --wait flag keeps the command active until Azure finishes processing the registration.
Before you check, which registration state do you expect now that the wait has finished?
- Confirm the provider registration state by running this command:
az provider show --namespace Microsoft.PolicyInsights --query "registrationState" --output table
What does this check show?
The query selects the provider's registration state from the Azure response. The table format keeps the evidence easy to read in Windows PowerShell.
You should see Registered in the output. That completes the subscription-side prerequisite for the policy add-on.
Provider not registered?
- Confirm the existing Windows PowerShell session still targets the intended Azure subscription.
- Repeat the registration command if the state remains incomplete.
- Check that your Azure role permits resource-provider registration.
Still blocked? Help me troubleshoot Microsoft.PolicyInsights provider registration in Azure
Enable policy on the cluster
The provider prepares the Azure subscription. The AKS add-on places the policy and admission components inside the validated cluster.
The add-on can take a few minutes to deploy. A busy terminal means AKS is installing the cluster components.
- Enable the Azure Policy add-on on the target cluster by running this command:
az aks enable-addons --addons azure-policy --name $CLUSTER_NAME --resource-group $RESOURCE_GROUP
What does this command do?
- The command enables the Azure Policy add-on on the existing AKS cluster.
- The $CLUSTER_NAME variable identifies the validated cluster.
- The $RESOURCE_GROUP variable keeps the change scoped to the cluster's existing resource group.
- Check the add-on profile by running this command:
az aks show --query addonProfiles.azurepolicy --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME
What does the add-on profile prove?
The query returns the Azure Policy portion of the cluster's add-on profile. An enabled value proves that AKS accepted the configuration change.
You should see JSON containing "enabled": true. The cluster is now configured to receive policy assignments.
Add-on not enabled?
- Confirm $RESOURCE_GROUP still contains the validated AKS resource group name.
- Confirm $CLUSTER_NAME still contains the validated AKS cluster name.
- Confirm the provider check returned Registered before retrying the add-on command.
Need another set of eyes? Help me troubleshoot an Azure Policy add-on that is not enabled on AKS
Verify both admission components
The add-on has two visible sides. Azure Policy distributes assignments from Azure while Gatekeeper evaluates Kubernetes admission requests inside the cluster.
Before you run the final checks, which pod status should prove that each side is healthy?
- Check the Azure Policy pods and Gatekeeper pods by running these commands:
kubectl get pods -n kube-system
kubectl get pods -n gatekeeper-system
What do these pod checks prove?
- The kube-system output shows the Azure Policy components that distribute policy artifacts to the cluster.
- The gatekeeper-system output shows the Gatekeeper admission components that evaluate workload requests.
In the first output, the azure-policy pod rows should show Running. In the second output, the Gatekeeper pod rows should also show Running.
Pods not running?
- Allow the add-on rollout more time if the pod rows show an in-progress state.
- Repeat the pod checks after the rollout has had time to finish.
- Confirm kubectl still targets the validated AKS cluster from the previous step.
Still waiting on healthy pods? Help me troubleshoot Azure Policy or Gatekeeper pods that are not running
- Keep both commands and the complete relevant pod rows visible in Windows PowerShell.
- Shorten only generated pod-name hashes with clearly labeled placeholders.
- Add a caption explaining that Azure Policy distributes assignments while Gatekeeper evaluates admissions.
The policy enforcement engine is live across Azure and your AKS cluster. Your two test manifests remain ready for the Terraform-managed security baseline that comes next.
Assign the Security Baseline
The Azure Policy add-on is running inside your AKS cluster. Gatekeeper is ready to evaluate admissions. A project-specific baseline assignment has not reached it.
Terraform turns the Linux pod security baseline into a repeatable resource-group assignment. This closes the gap between a running enforcement engine and an active governance rule.
In this step, get ready to:
- Create the reusable Terraform configuration.
- Review a plan for one Deny-mode policy assignment.
- Apply the assignment until Gatekeeper receives its constraint templates.
Create the Terraform configuration
Policy as code stores the assignment in files that you can review before making a governance change. The configuration keeps the initiative scope plus its enforcement effect reproducible.
- Create variables.tf inside the existing aks-policy-governance workspace.
- Define the two environment inputs by pasting this configuration into variables.tf:
variable "subscription_id" {
description = "Azure subscription ID that contains the existing AKS cluster."
type = string
}
variable "resource_group_name" {
description = "Name of the resource group that contains the existing AKS cluster."
type = string
}
Why use input variables?
The variables keep environment-specific values outside main.tf. You can reuse the assignment configuration with another approved AKS resource group by supplying different local values.
- Save variables.tf.
- Confirm the file shows declarations for subscription_id plus resource_group_name.
Missing one of the variables?
Check that both variable blocks have an opening brace plus a matching closing brace. Help me compare my variables file.
- Create main.tf inside the aks-policy-governance workspace.
- Configure the required Terraform version plus the AzureRM provider by pasting this first section into main.tf:
terraform {
required_version = ">= 1.6.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "= 5.3.0"
}
}
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}
What does the provider configuration do?
- The required_version constraint accepts Terraform 1.6.0 or later.
- The provider constraint pins AzureRM to 5.3.0.
- The provider uses var.subscription_id to target the intended Azure subscription.
- Save main.tf.
- Confirm the provider block reads the subscription ID from var.subscription_id.
Provider block looks incomplete?
Check that features {} remains inside the provider block. Help me check my AzureRM provider block.
- Add the resource-group lookup plus the baseline assignment below the provider block in main.tf by pasting:
data "azurerm_resource_group" "aks" {
name = var.resource_group_name
}
data "azurerm_policy_set_definition" "aks_pod_security_baseline" {
display_name = "Kubernetes cluster pod security baseline standards for Linux-based workloads"
}
resource "azurerm_resource_group_policy_assignment" "aks_pod_security_baseline" {
name = "aks-pod-security-baseline"
display_name = "Kubernetes cluster pod security baseline standards for Linux-based workloads"
description = "Enforces the AKS Linux pod security baseline for the lab resource group."
resource_group_id = data.azurerm_resource_group.aks.id
policy_definition_id = data.azurerm_policy_set_definition.aks_pod_security_baseline.id
parameters = jsonencode({
effect = {
value = "Deny"
}
})
}
How does the assignment work?
- The resource-group data source finds the Azure scope containing your existing AKS cluster.
- The policy-set data source finds Kubernetes cluster pod security baseline standards for Linux-based workloads by its exact display name.
- The assignment applies the initiative to the resource group with the Deny effect.
- Save main.tf.
- Confirm the assignment is named aks-pod-security-baseline.
- Confirm the parameters contain the Deny value.
Assignment block does not match?
Check that both data source names match their references in the assignment. Help me find a mismatched Terraform reference.
- Create outputs.tf inside the aks-policy-governance workspace.
- Expose the assignment resource ID by pasting:
output "policy_assignment_id" {
description = "Resource ID of the AKS pod security baseline assignment."
value = azurerm_resource_group_policy_assignment.aks_pod_security_baseline.id
}
Why expose the assignment ID?
The policy_assignment_id output gives you the Azure resource ID after the apply succeeds. It provides direct evidence that Terraform manages the governance resource.
- Save outputs.tf.
- Confirm the output references azurerm_resource_group_policy_assignment.aks_pod_security_baseline.id.
Output references the wrong resource?
Copy the assignment reference directly from main.tf so every segment matches. Help me repair the policy assignment output.
- Create terraform.tfvars.example inside the aks-policy-governance workspace.
- Document the required local inputs by pasting:
subscription_id = "00000000-0000-0000-0000-000000000000"
resource_group_name = "replace-with-your-aks-resource-group"
Why keep an example file?
The example records which values the configuration needs without tying the reusable files to one Azure environment. Your local terraform.tfvars receives the real values.
- Save terraform.tfvars.example.
- Confirm the example still contains both documented placeholders.
Example file has different values?
Restore the two placeholders shown above before creating the local copy. Help me restore the example variable file.
Your local variable file connects the reusable configuration to the intended subscription. Keeping that file out of screenshots prevents unnecessary exposure of environment-specific identifiers.
- Display the active Azure subscription again by running:
az account show
What does this check confirm?
The command shows the Azure subscription currently selected by the CLI. Use the visible ID only after confirming the subscription name matches your intended lab environment.
- Record the displayed subscription ID here: your Azure subscription ID.
- Record the resource group stored in $RESOURCE_GROUP here: your AKS resource group name.
- Create a local copy of terraform.tfvars.example named terraform.tfvars using your code editor's duplicate-file action.
You should now see both files in the aks-policy-governance workspace.
- Replace 00000000-0000-0000-0000-000000000000 in terraform.tfvars with your Azure subscription ID.
The first line should now contain your current subscription ID instead of the example value.
- Replace replace-with-your-aks-resource-group with your AKS resource group name.
The second line should now match the resource group identified by $RESOURCE_GROUP.
- Save terraform.tfvars.
- Keep terraform.tfvars outside every screenshot.
Local values do not match Azure?
Compare the first value with the ID from az account show. Compare the second value with $RESOURCE_GROUP.
Help me check my local Terraform values.
✔️ Awesome, I've got everything!
Your reusable Terraform files match the project configuration. Your separate terraform.tfvars contains the environment-specific values.
ⓧ I'd like to double check the full code
- Compare your complete main.tf with this reference:
terraform {
required_version = ">= 1.6.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "= 5.3.0"
}
}
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}
data "azurerm_resource_group" "aks" {
name = var.resource_group_name
}
data "azurerm_policy_set_definition" "aks_pod_security_baseline" {
display_name = "Kubernetes cluster pod security baseline standards for Linux-based workloads"
}
resource "azurerm_resource_group_policy_assignment" "aks_pod_security_baseline" {
name = "aks-pod-security-baseline"
display_name = "Kubernetes cluster pod security baseline standards for Linux-based workloads"
description = "Enforces the AKS Linux pod security baseline for the lab resource group."
resource_group_id = data.azurerm_resource_group.aks.id
policy_definition_id = data.azurerm_policy_set_definition.aks_pod_security_baseline.id
parameters = jsonencode({
effect = {
value = "Deny"
}
})
}
What should match in main.tf?
The file pins the provider. It looks up the resource group plus the built-in initiative. It creates one assignment with the Deny effect.
- Compare your complete variables.tf with this reference:
variable "subscription_id" {
description = "Azure subscription ID that contains the existing AKS cluster."
type = string
}
variable "resource_group_name" {
description = "Name of the resource group that contains the existing AKS cluster."
type = string
}
What should match in variables.tf?
The file defines the subscription ID plus resource-group name as string inputs.
- Compare your complete outputs.tf with this reference:
output "policy_assignment_id" {
description = "Resource ID of the AKS pod security baseline assignment."
value = azurerm_resource_group_policy_assignment.aks_pod_security_baseline.id
}
What should match in outputs.tf?
The file exposes the resource ID from azurerm_resource_group_policy_assignment.aks_pod_security_baseline.
- Compare your complete terraform.tfvars.example with this reference:
subscription_id = "00000000-0000-0000-0000-000000000000"
resource_group_name = "replace-with-your-aks-resource-group"
What should match in the example file?
The example retains the documented placeholders. Your local terraform.tfvars contains your current values.
- Confirm the existing nginx-privileged.yaml still matches this reference:
apiVersion: v1
kind: Pod
metadata:
name: nginx-privileged
spec:
containers:
- name: nginx-privileged
image: mcr.microsoft.com/oss/nginx/nginx:1.15.5-alpine
securityContext:
privileged: true
What should match in the privileged manifest?
The manifest still contains privileged: true under the container security context. This file remains unchanged for the after-policy test.
- Confirm the existing nginx-unprivileged.yaml still matches this reference:
apiVersion: v1
kind: Pod
metadata:
name: nginx-unprivileged
spec:
containers:
- name: nginx-unprivileged
image: mcr.microsoft.com/oss/nginx/nginx:1.15.5-alpine
What should match in the control manifest?
The control manifest uses the same container image without a privileged security context. This file also remains unchanged.
Initialize and review the plan
A saved Terraform plan shows the exact governance change before Azure receives it. The plan gives you a review point for the assignment scope plus enforcement effect.
The first initialization may take a few minutes while Terraform downloads the pinned provider. A short pause here is expected.
- Initialize the existing aks-policy-governance workspace by running:
terraform init
What does initialization do?
Terraform prepares the workspace. It also installs the AzureRM provider version allowed by the configuration.
You should see a successful initialization message before Windows PowerShell returns to the prompt.
Initialization failed?
- Check that your computer can reach the Terraform provider registry.
- Check that main.tf pins hashicorp/azurerm to 5.3.0.
Help me troubleshoot Terraform initialization.
- Format the Terraform configuration by running:
terraform fmt
What does formatting prove?
Terraform reads every configuration file before applying its standard layout. A clean return to the prompt confirms the formatter could parse the files.
Windows PowerShell may list files whose spacing changed. It may also return directly to the prompt when everything already matches the standard layout.
Seeing a formatting error?
- Check every opening brace for a matching closing brace.
- Check that each quoted string has a closing quotation mark.
Help me find the HCL syntax problem.
- Validate the complete configuration by running:
terraform validate
What does validation check?
Validation checks the configuration structure plus its provider arguments. It catches broken references before Terraform calculates an Azure change.
You should see confirmation that the configuration is valid.
Configuration is not valid?
- Check that subscription_id uses the same spelling in every file.
- Check that resource_group_name uses the same spelling in every file.
- Check the full-code references above for any missing blocks.
Help me fix the Terraform validation failure.
Before you create the plan, how many Azure resources do you expect Terraform to add?
- Create a reviewable execution plan by running:
terraform plan -out main.tfplan
What does the saved plan capture?
Terraform compares the desired assignment with the current Azure state. The saved main.tfplan preserves that reviewed change for the apply command.
You should see one proposed addition with no changes or destructions. The proposed resource should be azurerm_resource_group_policy_assignment.aks_pod_security_baseline.
- Confirm the plan shows the assignment name aks-pod-security-baseline.
- Confirm the plan shows Kubernetes cluster pod security baseline standards for Linux-based workloads.
- Confirm the parameters show the Deny effect.
- Replace subscription IDs plus resource IDs with labeled placeholders in the screenshot.
Plan shows an unexpected change?
- Stop before applying the saved plan.
- Check that terraform.tfvars names the intended AKS resource group.
- Check that the assignment resource appears once in main.tf.
Help me review the unexpected Terraform plan.
Apply the assignment and verify synchronization
Applying this plan creates a governance control at the AKS resource group's scope. This is a sensitive change because the Deny effect can reject noncompliant admissions after synchronization.
You have already reviewed the exact resource in the saved plan. Terraform now applies that same reviewed change without recalculating it.
- Apply the saved plan by running:
terraform apply main.tfplan
What does the apply create?
Terraform creates one resource-group-scoped assignment for the built-in Linux pod security baseline initiative. The assignment keeps the Deny effect under version-controlled configuration.
You should see Apply complete! Resources: 1 added, 0 changed, 0 destroyed. The output should also include policy_assignment_id with the new assignment resource ID.
Apply did not create the assignment?
- Confirm Windows PowerShell remains signed in to the intended Azure subscription.
- Confirm your Azure role permits policy assignments at the target resource-group scope.
- Confirm main.tfplan still exists in the aks-policy-governance workspace.
Help me troubleshoot the policy assignment apply.
Policy synchronization can take up to 20 minutes. The wait means Azure Policy is distributing the assignment to the cluster.
Before you check, do you expect the baseline initiative to create one constraint template or several?
- Check for synchronized constraint templates by running:
kubectl get constrainttemplates
What does this check prove?
ConstraintTemplates describe the admission checks that Gatekeeper can evaluate. Names beginning with k8sazure prove that the Azure Policy assignment has reached the cluster.
When synchronization completes, you should see multiple template names beginning with k8sazure.
- Wait a few minutes if the populated template table is not visible yet.
- Repeat the same command until multiple k8sazure templates appear.
- Capture the complete apply summary plus the populated constraint-template table in Windows PowerShell.
- Replace environment-specific IDs plus timestamps with labeled placeholders in the screenshot.
Templates still missing?
- Allow the full synchronization window before treating the empty result as a failure.
- Confirm the Azure Policy pods remain Running in kube-system.
- Confirm the Gatekeeper pods remain Running in gatekeeper-system.
Help me diagnose missing k8sazure templates.
That is the governance layer in place. Terraform manages the Deny assignment. Gatekeeper has received the admission templates. Next, you will submit both test manifests to prove the unsafe pod is blocked while the compliant pod still runs.
Prove Deny Enforcement
Your Terraform-managed Deny assignment has synchronized into the AKS cluster. You can now test whether Azure Policy changes the deployment path that accepted the privileged Pod earlier.
This step tests both sides of admission control. Open Policy Agent Gatekeeper should reject the unsafe manifest.
Kubernetes should still run the normal Pod. Together, those outcomes prove the policy protects the cluster without stopping compliant work.
In this step, get ready to:
- Resubmit the privileged manifest to prove Gatekeeper denies it.
- Deploy the compliant manifest to prove allowed workloads still run.
- Capture the policy evidence before deleting the temporary Pod.
Test the privileged admission
The privileged manifest is unchanged from your before-policy test. Its securityContext.privileged: true setting gives the new admission control a direct comparison.
Before you run this check, do you expect Kubernetes to create the Pod?
- Submit nginx-privileged.yaml again by running this command:
kubectl apply -f nginx-privileged.yaml
What Does This Test Prove?
- The kubectl command submits the unchanged manifest to the current cluster.
- Gatekeeper evaluates the privileged setting during admission.
- The Deny effect stops a matching request before Kubernetes creates the Pod.
You'll see admission webhook "validation.gatekeeper.sh" denied the request in the wrapped output. The denial also states Privileged container is not allowed.
You have now proved that the same unsafe manifest accepted earlier is blocked during admission.
Was the Privileged Pod Created?
- Confirm your current Kubernetes context still points to the validated AKS cluster.
- Check that the policy assignment effect remains Deny.
- Confirm multiple constraint templates beginning with k8sazure remain available.
Still seeing the Pod get created? Help me diagnose the missing denial
- Resize Windows PowerShell so the complete wrapped denial is visible.
- Shorten only the generated policy hash with a labeled placeholder.
Prove the compliant path
A Deny policy needs a control test that stays allowed. The unprivileged manifest uses the same container image without requesting privileged access.
Before you run these commands, do you expect the compliant Pod to reach Running?
- Create the compliant Pod and check its status by running these commands:
kubectl apply -f nginx-unprivileged.yaml
kubectl get pod nginx-unprivileged
What Does This Control Test Prove?
- The nginx-unprivileged.yaml manifest uses the same image without the privileged security context.
- The first command submits an allowed workload through the same admission path.
- The second command reads the resulting Pod status from the cluster.
Windows PowerShell first confirms that Kubernetes created the Pod. The row for nginx-unprivileged then shows 1/1 under READY with Running under STATUS.
You have now proved the compliant path survives Deny enforcement.
Is the Compliant Pod Not Running?
- Allow the container image pull to finish before checking the Pod again.
- Confirm the Pod name in the output matches nginx-unprivileged.
- Inspect the Pod events if the status remains stalled.
Need help interpreting the Pod status? Help me troubleshoot the compliant workload
- Keep the successful creation output visible in Windows PowerShell.
- Keep the complete nginx-unprivileged Pod row visible in the same image.
Capture final evidence and clean up
The admission results prove policy behavior. A final ConstraintTemplate listing connects that behavior to the synchronized enforcement engine.
- Display the synchronized ConstraintTemplates by running this command:
kubectl get constrainttemplates
What Does This Listing Prove?
- The Azure Policy add-on distributes the templates that define Kubernetes policy checks.
- Gatekeeper uses those templates to evaluate admission requests.
- Multiple names beginning with k8sazure show that the assignment remains synchronized.
You'll see multiple ConstraintTemplate names beginning with k8sazure.
- Keep the command and complete relevant ConstraintTemplate rows visible in Windows PowerShell.
The cleanup removes only the temporary nginx-unprivileged Pod. Your manifests remain in the workspace.
The Terraform files stay unchanged. The Deny assignment remains active.
- Delete the temporary compliant Pod by running this command:
kubectl delete -f nginx-unprivileged.yaml
What Does This Cleanup Change?
- The command deletes the Pod object described by nginx-unprivileged.yaml.
- The YAML manifest remains available for future demonstrations.
- The Terraform-managed policy assignment continues protecting the cluster.
Windows PowerShell confirms that Kubernetes deleted the temporary Pod.
Was the Pod Already Missing?
A message saying the Pod cannot be found means the temporary workload is already absent. Your policy assignment remains unaffected.
Want to confirm the cleanup state? Help me verify the Pod deletion
Before the final check, do you expect the temporary Pod to remain in the list?
- Verify the temporary Pod is gone by running this command:
kubectl get pods
What Does This Final Check Prove?
- The kubectl command lists the Pods that remain in the current namespace.
- The absence of nginx-unprivileged confirms that the temporary control workload is deleted.
You should no longer see a row named nginx-unprivileged. The privileged Pod remains absent because Gatekeeper rejected it before creation.
That is the governance proof complete. Your evidence shows the unsafe request denied while the compliant workload remains deployable.
Secret mission
Add an Operator-Friendly Denial Message
A blocked workload is easier to fix when the policy explains what its owner needs to change. Add remediation guidance to the existing assignment while preserving Deny enforcement.
Clean Up Your Resources
Clean Up Your Resources
Using Azure Policy on Azure resources adds no charge. Decide whether to keep the controls active, pause enforcement for later, or delete the controls entirely.
Cost warning
Your existing AKS cluster, nodes, networking, logs, and related services continue to incur their normal charges. The policy assignment adds no extra Azure Policy charge.
Deleting these project controls leaves your existing cluster in place. Costs from earlier resources continue until those resources are stopped or deleted.
Resources you used:
- Terraform-managed Azure Policy assignment aks-pod-security-baseline with Deny enforcement and an operator-friendly non-compliance message.
- Azure Policy add-on with Open Policy Agent Gatekeeper components on the existing AKS cluster.
- Local aks-policy-governance folder containing the Terraform configuration and Kubernetes manifests.
Keep everything running
No action needed. Choose this if you want the cluster to remain protected for future governance tests.
- Keep aks-pod-security-baseline in Deny mode so privileged admissions remain blocked.
- Leave the Azure Policy add-on enabled so Gatekeeper continues to evaluate Kubernetes admissions.
- Retain the aks-policy-governance folder so you can review or update the control through Terraform.
Pause - I'll come back to this later
Switch the assignment to Audit when you need compliance visibility without rejected admissions.
- In main.tf, find the line containing value = "Deny".
- Replace value = "Deny" with value = "Audit".
- Save main.tf.
- Format the configuration and create a reviewable plan by running:
terraform fmt
terraform validate
terraform plan -out main.tfplan
What do these commands prove?
- terraform fmt keeps the configuration consistently formatted.
- terraform validate confirms that the updated configuration is valid.
- terraform plan -out main.tfplan saves the reviewed enforcement change for the next command.
- Inspect the plan and confirm that aks-pod-security-baseline changes from Deny to Audit.
- Apply the reviewed plan by running:
terraform apply main.tfplan
What does this command change?
Terraform applies the saved plan to the existing assignment. The assignment remains managed while its effect changes to Audit.
You should see Terraform report one changed resource and zero destroyed resources. Your centralized compliance reporting remains active.
A quiet wait here is expected. Policy synchronization can take up to 20 minutes before new admissions stop being denied.
Delete - I don't want to use this again
Remove the project controls when you no longer need this policy assignment or its local files. Your existing AKS cluster remains intact.
- Create a reviewable destroy plan for the Terraform-managed assignment by running:
terraform plan -destroy -out main.destroy.tfplan
What does this plan contain?
The destroy plan targets the Azure Policy assignment recorded in the current Terraform state. It gives you a final review before any cloud control is removed.
- Review the plan and confirm that only aks-pod-security-baseline is marked for destruction.
- Remove the reviewed assignment by running:
terraform apply main.destroy.tfplan
What does this command remove?
Terraform removes the resource-group policy assignment from Azure. The existing AKS cluster and its other resources remain in place.
That's the Terraform-managed control removed. You should see Terraform confirm that one resource was destroyed.
The add-on can support policy assignments beyond this project. Removing it is safe only when no other assignments depend on it.
- Disable the Azure Policy add-on only if no other assignments depend on it by running:
az aks disable-addons --addons azure-policy --name $CLUSTER_NAME --resource-group $RESOURCE_GROUP
What does disabling the add-on do?
This removes the Azure Policy add-on and its Gatekeeper enforcement components from the existing AKS cluster. Azure CLI should confirm that the add-on was disabled.
Your local folder still contains the project configuration and Kubernetes manifests after the cloud controls are gone.
- Close the Windows PowerShell window after the cloud teardown finishes.
- Locate aks-policy-governance in Windows File Explorer.
- Select the aks-policy-governance folder.
- Press Delete to remove the folder.
- Confirm that aks-policy-governance no longer appears in Windows File Explorer.
Nice Work!
Nice Work!
Outstanding work! You turned an existing Azure Kubernetes Service (AKS) cluster into a governed deployment target with Azure Policy.
Your evidence now shows Open Policy Agent Gatekeeper rejecting a privileged pod while a compliant pod still reaches Running.
You've learned how to:
- Produce before-policy evidence that nginx-privileged.yaml reaches Running on the ungoverned cluster.
- Manage the built-in Linux pod security baseline as policy as code with Terraform at the AKS resource group scope using the Deny effect.
- Build a compliance evidence set in Windows PowerShell that shows the privileged request denied by admission control while nginx-unprivileged.yaml reaches Running.
- Secret Mission: Add a remediation-focused non-compliance message that tells workload owners how to remove privileged access without weakening the Deny effect.
Ready to quiz yourself?