Source maintenance reviewed September 4, 2026: this updated source passed its offline and CI checks. Historical tenant evidence below retains its original dates; no fresh tenant deployment or detection result is asserted here.
Overview
This lab deploys a complete AKS runtime security stack using Microsoft Defender for Cloud:
- Gated Deployment (GA) โ Evaluates images at admission; configured rules can audit or deny matching images
- Binary Drift Detection and Blocking (GA within documented support constraints) โ Detects external executables and can alert on or block them after the policy and prerequisites are configured
- Container Anti-Malware (GA) โ Detects and blocks malware inside running containers
- Sentinel Analytics Rules โ 3 owner-marked rules for binary drift, malware, and
kubectl exec, deployed disabled until explicitly enabled - Sentinel Workbook โ Container Runtime Security Dashboard
Prerequisites
- Azure subscription with Owner or Contributor + User Access Administrator
- Azure CLI v2.60+
- kubectl compatible with AKS Kubernetes 1.35
- Helm v3.12+
- PowerShell 7.4 or later, enforced before either helper runs
- Your actual public VPN/NAT egress IPv4 CIDRs for API server access
- Permission to read the lab Log Analytics workspace ID and shared key for Helm sensor telemetry
The lab uses a Helm-managed sensor, so the operator owns chart upgrades. The
portal can label a Helm-installed subscription not supported for Gated deployment; if the rule flow offers to auto-install components, choose
Skip to avoid conflicting with the existing Helm deployment. Gated
deployment also requires Kubernetes 1.31 or later, OIDC on AKS, Defender sensor
with Security Gating, Registry access with Security findings, and vulnerability
findings artifacts for evaluated images. Verify these current service-side
prerequisites before attributing an allow or denial to the policy.
Quick Start
git clone https://github.com/j-dahl7/aks-runtime-security-lab.git
cd aks-runtime-security-lab
git checkout 47ea45cae2372a2333b4a105e8daee1d3a073e63
# Replace this placeholder with your actual public operator egress.
$operatorEgress = @('<your-public-egress-ip>/32')
# Preview only: no Azure, kubeconfig, Kubernetes, Helm, or secret-file mutations
./scripts/Deploy-Lab.ps1 -Location "eastus" -ProjectName "aks-runtime-lab" -ApiServerAuthorizedIpRanges $operatorEgress -WhatIf
# If the required paid subscription settings are not already enabled, review
# the active subscription and pricing before explicitly authorizing the change.
$env:CONFIRM_SUBSCRIPTION_SCOPE = "ENABLE-DEFENDER-FOR-CONTAINERS"
# Deploy infrastructure, sensor, disabled Sentinel rules, and workbook
./scripts/Deploy-Lab.ps1 -Location "eastus" -ProjectName "aks-runtime-lab" -ApiServerAuthorizedIpRanges $operatorEgress
First deployment requires 1โ20 explicit public IPv4 CIDRs, each /24 through /32. The helper rejects broad, private, reserved, documentation, benchmark, malformed, and duplicate ranges; it does not discover your egress or allow all addresses by default. Confirm the actual VPN/NAT egress with your network owner. Owned reruns reuse the exact recorded ranges when omitted and reject changed inputs or live drift. The API endpoint remains public behind its IP allowlist and retains local Kubernetes authentication; this is not a private or Entra-only deployment.
Before testing, configure both portal-owned controls. Change the default binary drift action from Ignore drift detection to the GA Drift detection alert action. Use the GA Drift detection blocking action only after confirming the current cluster, cloud, sensor, policy, and network prerequisites. Then create a gated-deployment vulnerability rule under Environment Settings > Security Rules; start with Audit, validate its decisions in Gated deployment > Admission Monitoring, and move to Deny only after confirming scope and thresholds. The deployment script does not create either policy.
# After reviewing the queries, explicitly enable the three rules
./scripts/Deploy-Lab.ps1 -Location "eastus" -ProjectName "aks-runtime-lab" -EnableSentinelRules
# Run test scenarios
./scripts/Test-RuntimeSecurity.ps1 -ProjectName "aks-runtime-lab" -Namespace "runtime-security-tests"
What Gets Deployed
| Resource | Type | Purpose |
|---|---|---|
aks-runtime-lab | AKS Cluster | Kubernetes 1.35, single-node cluster |
defender-k8s | Helm release | Pinned Defender chart 0.11.4 with anti-malware collector |
aks-runtime-lab-law | Log Analytics | Container Insights + Sentinel |
| Defender for Containers | Security Plan | Subscription-level enablement |
| 3 Analytics Rules | Sentinel | Owner-marked and disabled by default; binary drift, malware, kubectl exec |
| 1 Workbook | Sentinel | Container Runtime Security Dashboard |
Test Scenarios
Test 1: Binary Drift
./scripts/Test-RuntimeSecurity.ps1 -SkipMalware -SkipGated
Validation boundary: the activity should be evaluated when binary drift is enabled, but alert/block behavior depends on the selected policy. Generation and ingestion are asynchronous; this lab does not promise a fixed latency.
Test 2: Anti-Malware (EICAR)
./scripts/Test-RuntimeSecurity.ps1 -SkipDrift -SkipGated
Validation boundary: runtime anti-malware evaluates execution, not the file write alone. EICAR is harmless, and a nonzero container command does not itself prove Defender blocked it. Inspect the policy action and Defender security alerts; policy changes can take up to 30 minutes to reach sensors, and alert latency varies.
Test 3: Gated Deployment
./scripts/Test-RuntimeSecurity.ps1 -SkipDrift -SkipMalware
Validation boundary: this is Microsoft’s documented gated-deployment test image. A matching Deny rule returns an admission-webhook denial; Audit allows the request. Review the decision, rule, image digest, violations, and exemptions in Gated deployment > Admission Monitoring. An allow can also mean a scope mismatch, missing findings artifact, or incomplete prerequisite.
Deployment Safety
-WhatIfexits after a preview-only summary and performs zero mutations.- A per-project, owner-only manifest is written before cloud mutation. Existing resource groups are rejected unless the exact manifest token matches their ownership tag; cleanup uses that same proof.
- Runtime testing verifies the tenant/subscription, ARM resourceUID, server/CA fingerprint, kube-system namespace UID, ownership, and live API allowlist before mutation. A matching context alias alone is insufficient. Older manifests require an owned deployment rerun with explicit API ranges.
- Foreign namespaces and pods are refused. The helper creates unique owner-labeled pods, checks UID before exec, and never pre-deletes existing pods. Review its printed run selector before manual pod cleanup.
- Azure calls bind the captured subscription; Kubernetes and Helm calls bind the verified context. Keep the local manifest trusted and avoid concurrent kubeconfig definition or pod administration; exec has no atomic UID precondition. Recorded identity changes, including certificate rotation, require independent operator review.
- The script checks for conflicting auto-provision policy assignments, other Defender Helm releases, and exact known stale cluster-scoped resources before changing protection.
- Before any deployment mutation, the script reads the subscription-wide Defender for Containers object. It preserves unrelated extensions and supported properties, refuses unknown properties rather than dropping them, and exits unless settings already match or
CONFIRM_SUBSCRIPTION_SCOPEexactly authorizes the paid shared-setting change. - Log Analytics credentials go through an owner-only temporary values file, never Helm process arguments, and are deleted on success or failure.
- Helm uses
--atomic. If installation fails after the managed profile or exclusion tag changes, the script restores the captured Defender workspace/profile and exact prior tag state. - Sensor deployment succeeds only when matching pods reach both Running and Ready state.
- All Sentinel objects are preflighted before the first Sentinel write. Rules and the workbook use deterministic manifest IDs and provenance; foreign collisions and any failed write stop the deployment.
- Run
pwsh ./tests/Test-DeployLabSafety.ps1for offline deployment, cleanup, context-drift, and failure mocks, andpwsh ./tests/Test-RuntimeSafety.ps1for offline foreign-target refusal and all three owned runtime scenarios. These tests submit no Azure or Kubernetes activity.
Estimated Cost
| Resource | Cost driver |
|---|---|
| AKS | One Standard_D4s_v3 node plus regional control-plane/network charges |
| Defender for Containers | Current subscription plan and protected vCores |
| Log Analytics | Ingestion and retention in the selected region |
Pricing and included allowances change. Confirm the active subscription, region, and current Azure pricing before deployment; destroy the lab when it is not in use.
Cleanup
./scripts/Deploy-Lab.ps1 -ProjectName "aks-runtime-lab" -Destroy -WhatIf
./scripts/Deploy-Lab.ps1 -ProjectName "aks-runtime-lab" -Destroy
Cleanup validates the exact manifest, resource-group owner token, and shared pricing before deletion. It deletes the owned group and verifies its absence before restoring captured pre-lab Defender pricing/extensions. Failed or incomplete deletion keeps protection enabled and retains the manifest. Restore rechecks shared-setting drift and verifies the result before removing the manifest. A retry accepts an exact already-restored before-state; other shared changes are never overwritten.
Companion Blog Post
AKS Runtime Security: Binary Drift, Anti-Malware & Gated Deployment with Defender for Cloud
