Skip to content

Jamonygr/Azure-Security-Lab

Repository files navigation

Azure Security Lab

Terraform Azure GitHub Actions License

Azure Security Lab banner

Azure Security Lab architecture

Build a practical Azure security lab with Terraform. The lab creates a small hub-spoke environment where an untrusted test client reaches a protected workload only through controlled network paths. Azure Firewall Basic sits in the hub, NSGs and route tables enforce segmentation, Key Vault stores generated secrets, Log Analytics collects diagnostics, and Azure Policy audits posture.

The design follows the same style as the Azure Landing Zone Lab: clear feature toggles, reusable modules, diagrams, wiki pages, CI checks, and hands-on validation steps.

Cost note: Azure Firewall is the main cost driver. In West Europe, Azure Firewall Basic is roughly 0.40 USD/hour plus data processing, so a full month can be about 292 USD before other resources. Deploy it for practice, validate the scenario, then destroy the lab.

Wiki-Style Knowledge Map

Use this repository like a compact security-lab encyclopedia: the root README is the landing article, and the wiki/ folder is the linked body of knowledge.

Topic Start here Deep dive
Architecture Architecture Architecture overview, addressing and routing, security controls
Technology stack What Gets Deployed Technology stack, module map, variables
Deployment lifecycle Quick Start Deploy the lab, apply-test-destroy runbook, final validation checklist
Network segmentation Lab Scenarios Firewall east-west routing, private access with Bastion
Secrets and identity Security Features Key Vault secrets, Entra test users
Detection and evidence Testing the Lab Security test matrix, Log Analytics detection, KQL examples, validation report
Governance and cost Cost Estimation Policy posture, cost control, GitHub Actions
Terminology References Glossary, Microsoft Learn and product references

Master Control Panel

The main toggles live in terraform.tfvars.example and environments/lab.tfvars.

deploy_firewall                = true
firewall_sku_tier              = "Basic"
force_spoke_egress_to_firewall = false

enable_test_client_public_ip = true
deploy_bastion               = false

deploy_entra_test_users   = false
deploy_conditional_access = false
deploy_sentinel           = false

deploy_keyvault      = true
deploy_log_analytics = true
deploy_policy        = true
deploy_budget        = true

The default keeps the lab easy to access with a restricted public RDP rule on the test client. Set admin_source_ip_cidr to your own public IP with /32.

Table of Contents

Overview

This Terraform project creates a focused security lab, not a full enterprise landing zone. It is built to make security controls visible and testable without deploying a large stack.

Core Components

  • Hub VNet: central connectivity and Azure Firewall subnet.
  • Test spoke: untrusted Windows test client with optional restricted public RDP.
  • Protected spoke: Windows IIS web VM without public IP.
  • Azure Firewall Basic: east-west routing, web allow rules, explicit admin-protocol deny example.
  • NSGs: subnet-level allow and deny rules.
  • Route tables: force test-to-protected and protected-to-test traffic through the firewall.
  • Key Vault: generated Windows admin password and lab details.
  • Log Analytics: firewall and Key Vault diagnostics.
  • Azure Policy: audit-mode posture controls.
  • Budget: subscription budget alerts through Owner contact role.

Optional Components

Component Toggle Default Notes
Azure Bastion deploy_bastion false Easier private access, but adds hourly cost.
Entra test users/groups deploy_entra_test_users false Requires directory permissions and entra_domain_name.
Conditional Access deploy_conditional_access false Report-only MFA policy, requires Entra ID P1/P2.
Microsoft Sentinel deploy_sentinel false Useful for SOC labs, but ingestion can increase cost.
Spoke default egress via firewall force_spoke_egress_to_firewall false Keep false when using public RDP to avoid asymmetric routing.

Architecture

Architecture overview

Internet
   |
   | restricted RDP from admin_source_ip_cidr
   v
Test Spoke 10.23.0.0/16
   |
   | UDR: protected spoke via firewall
   v
Hub VNet 10.20.0.0/16
   |
   | Azure Firewall Basic
   v
Protected Spoke 10.22.0.0/16
   |
   | private web VM, no public IP
   v
Protected workload

Address Plan

Zone CIDR Purpose
Hub 10.20.0.0/16 Firewall and optional Bastion
Azure Firewall subnet 10.20.1.0/24 Required reserved subnet name
Azure Firewall management subnet 10.20.3.0/24 Required by Azure Firewall Basic
Azure Bastion subnet 10.20.2.0/26 Created only when Bastion is enabled
Protected spoke 10.22.0.0/16 Private workload
Protected web subnet 10.22.1.0/24 Windows IIS web VM
Test spoke 10.23.0.0/16 Untrusted client
Test client subnet 10.23.1.0/24 Windows test VM

Lab Scenarios

Lab scenarios

The wiki now includes a Microsoft Learn-style path with objectives, diagrams, exact validation commands, expected results, troubleshooting, cleanup, and product reference links. Start with the wiki index, then use the final validation checklist before sharing or pushing the lab. For a pre-commit proof pass, use the final-check runbook and the security test matrix.

Scenario 1: Firewall in the Middle

Objective: prove that private spoke-to-spoke traffic crosses Azure Firewall.

Expected result:

  • curl http://PROTECTED_PRIVATE_IP succeeds from the test client.
  • RDP or WinRM from test to protected fails.
  • Firewall logs show allowed web flows and denied admin attempts.

Scenario 2: Network Segmentation

Objective: understand how NSGs and UDRs work together.

Controls:

  • Protected VM has no public IP.
  • Protected NSG only allows HTTP/S from the test spoke.
  • Route tables send private spoke traffic through the firewall when deploy_firewall = true.

Scenario 3: Key Vault Secrets

Objective: store generated lab access material safely.

Secrets include:

  • windows-admin-password
  • test-client-private-ip
  • protected-web-private-ip
  • protected-web-test-url
  • firewall IPs when deployed
  • Entra test user passwords when enabled

Scenario 4: Entra Test Identity

Objective: create disposable users and groups for identity/security practice.

Users:

  • sec-admin
  • sec-analyst
  • lab-user
  • break-glass

Groups:

  • security-admins
  • security-analysts
  • lab-users
  • break-glass

This scenario is disabled by default because it needs directory permissions and a verified domain.

Scenario 5: Policy Posture

Objective: practice audit-mode governance without breaking the lab.

Policies audit:

  • required environment tag
  • allowed locations
  • public IP creation
  • storage accounts without HTTPS-only traffic

Scenario 6: Detection Basics

Objective: inspect security telemetry in Log Analytics.

Included starter query:

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.NETWORK"
| where Category in ("AzureFirewallNetworkRule", "AZFWNetworkRule") or Category contains "Firewall"
| where msg_s has "Deny"
| take 50

What Gets Deployed

Default Resources

Area Resources
Resource groups hub, test, protected, security
Networking 3 VNets, subnets, peerings, NSGs, route tables
Security Azure Firewall Basic, firewall policy, rule collections
Compute 1 test Windows VM, 1 protected Windows IIS web VM
Secrets Key Vault, generated Windows admin password, lab output secrets
Monitoring Log Analytics workspace, firewall diagnostics, Key Vault diagnostics
Governance custom audit policies and assignments
Cost subscription budget alert

Deployment Profiles

Profile Key toggles Approximate cost behavior
Minimal private test deploy_firewall=false Low cost, no firewall scenario
Default security lab deploy_firewall=true, deploy_bastion=false Firewall is the main cost
Private access lab deploy_bastion=true, enable_test_client_public_ip=false More secure access, more cost
Identity lab deploy_entra_test_users=true Requires Graph permissions
SOC lab deploy_sentinel=true Higher detection value, possible ingestion cost

Quick Start

Prerequisites

  • Terraform >= 1.9.0
  • Azure CLI
  • An Azure subscription
  • Contributor rights for the subscription or target scope
  • Optional: Entra permissions if creating test users or Conditional Access

1. Configure Azure

az login
az account set --subscription "<subscription-id>"

2. Configure the lab

Copy the example file:

Copy-Item terraform.tfvars.example terraform.tfvars

Set your public IP:

admin_source_ip_cidr = "YOUR_PUBLIC_IP/32"

3. Deploy

terraform init
terraform plan -var-file="terraform.tfvars"
terraform apply -var-file="terraform.tfvars"

4. Get access details

terraform output test_client
terraform output protected_web
terraform output -raw windows_admin_password

The Windows admin password is sensitive. You can also retrieve it from Key Vault when deploy_keyvault = true.

5. Destroy when done

terraform destroy -var-file="terraform.tfvars"

Validated Environment Run

The lab was applied, tested, and destroyed on June 7, 2026 from this workspace against the active Azure subscription.

Check Result
Toolchain Terraform 1.12.0, Azure CLI 2.83.0
Static checks terraform fmt -check -recursive passed; terraform validate passed
Plan Initial Windows plan: 69 to add, 0 to change, 0 to destroy; recovery plan after the hostname fix: 4 to add, 0 to change, 0 to destroy
Apply Shared lab resources were created first; recovery apply created the two Windows VMs and Custom Script Extensions
Runtime test Protected IIS active; test client HTTP to protected private IP succeeded; TCP/3389 to protected VM returned ADMIN_PORT_BLOCKED
Telemetry Log Analytics showed firewall Allow for test-to-protected-web and Deny for deny-test-to-protected-admin
Destroy 69 destroyed; all four default resource groups returned false; generated local state and plan files were removed

See the full environment test report for exact commands, findings, and the Log Analytics REST workaround used for this Azure CLI install.

GitHub Actions

The repo includes .github/workflows/terraform-checks.yml for non-destructive static checks:

  • Terraform formatting
  • Terraform backend-free init and validation
  • TFLint
  • Checkov
  • Gitleaks

The workflow runs on pushes to main, pull requests, and manual dispatch. It does not apply or destroy Azure resources.

No Azure credentials are required for the default checks. If you later add a plan workflow, use GitHub Actions OIDC with Azure and remote Terraform state.

Variable Purpose
ARM_CLIENT_ID App registration or managed identity client ID for optional Azure OIDC workflows
ARM_TENANT_ID Entra tenant ID for optional Azure OIDC workflows
ARM_SUBSCRIPTION_ID Azure subscription ID for optional Azure OIDC workflows
DESTROY_CONFIRM Recommended explicit guard if a future manual destroy workflow is added

The current backend is local for easy workstation use. Before shared CI plan/apply/destroy usage, replace backend.tf with an Azure Storage backend so pipeline state is durable.

Configuration Options

Important variables:

Variable Default Description
project security Naming prefix
environment lab Environment label
location westeurope Azure region
deploy_firewall true Deploy Azure Firewall
firewall_sku_tier Basic Firewall SKU
admin_source_ip_cidr 0.0.0.0/32 Allowed RDP source
enable_test_client_public_ip true Public RDP path to test client
deploy_bastion false Optional private access
deploy_entra_test_users false Optional Entra users/groups
deploy_conditional_access false Optional report-only MFA policy
deploy_sentinel false Optional Sentinel onboarding
deploy_keyvault true Store generated lab secrets
deploy_log_analytics true Enable central logs
deploy_policy true Enable audit policies
deploy_budget true Enable budget alert

Testing the Lab

Traffic flow

Static Checks

terraform fmt -check -recursive
terraform init -backend=false -input=false
terraform validate

Before plan, apply, or destroy, initialize the configured local backend:

terraform init -input=false

Optional tools:

tflint --init
tflint --recursive
checkov -d . --framework terraform
gitleaks detect --source . --redact

Terraform Plan

terraform plan -var-file="environments/lab.tfvars"

Review that:

  • Azure Firewall appears only when deploy_firewall = true.
  • Protected VM has no public IP.
  • Test and protected route tables point private spoke traffic at the firewall private IP.
  • Entra users are skipped when deploy_entra_test_users = false.

Manual Connectivity

From the test client:

Invoke-WebRequest -UseBasicParsing "http://PROTECTED_PRIVATE_IP"
Test-NetConnection PROTECTED_PRIVATE_IP -Port 3389

Expected:

  • HTTP succeeds.
  • RDP to protected fails.

Terratest

Integration tests live in tests/.

cd tests
go test ./...

Tests skip automatically when ARM_SUBSCRIPTION_ID is not set.

Security Features

  • Private protected workload with no public IP.
  • Restricted public RDP to test client via admin_source_ip_cidr.
  • NSG deny rules for administrative protocols.
  • Firewall policy allow and deny examples.
  • Key Vault secret storage for generated Windows admin password and lab values.
  • Log Analytics diagnostics for firewall and Key Vault.
  • Audit-mode Azure Policy assignments.
  • Optional Entra test identity and report-only Conditional Access.
  • Optional Sentinel onboarding.
  • GitHub Actions validation, linting, secret scanning, and security scanning.

Cost Estimation

Costs vary by region and usage. The largest default cost is Azure Firewall Basic.

Resource Default Cost behavior
Azure Firewall Basic On Roughly 0.40 USD/hour in West Europe plus data
2 small Windows VMs On Low, but still billable while running
Log Analytics On Controlled by log_analytics_daily_quota_gb
Key Vault On Usually low for lab usage
Bastion Off Adds hourly cost when enabled
Sentinel Off Can add ingestion/analytics cost

Cost tips:

  • Destroy the lab after practice.
  • Keep deploy_bastion = false unless you need private access.
  • Keep deploy_sentinel = false until you are ready for detection scenarios.
  • Use the budget alert as an early warning, not as a hard cost limit.
  • Consider deploy_firewall = false when working only on docs, policy, or identity modules.

Troubleshooting

Cannot RDP to test client

  • Confirm enable_test_client_public_ip = true.
  • Confirm admin_source_ip_cidr is your current public IP with /32.
  • Confirm the test client public IP from terraform output test_client.

Public RDP breaks after forcing egress

If force_spoke_egress_to_firewall = true, public RDP return traffic can become asymmetric. Use Bastion/private access, or keep the default false.

Protected web is not reachable

  • Confirm the protected VM private IP from terraform output protected_web.
  • Confirm route tables exist when deploy_firewall = true.
  • Confirm firewall rules allow TCP 80/443 from 10.23.0.0/16 to 10.22.0.0/16.
  • Confirm Custom Script Extension completed on the protected VM.

Entra user creation fails

  • Set deploy_entra_test_users = false if you do not need identity scenarios.
  • Set entra_domain_name to a verified tenant domain.
  • Confirm the Terraform identity has permission to create users and groups.

Conditional Access fails

Conditional Access requires Entra ID P1/P2 licensing and the right Graph permissions. It is disabled by default.

Budget creation fails

Budget APIs can require subscription-level permissions. Set deploy_budget = false if your account cannot create budgets.

Project Structure

.
|-- .github/workflows/terraform-checks.yml
|-- README.md
|-- backend.tf
|-- environments/
|-- docs/images/
|-- modules/
|   |-- bastion/
|   |-- budget/
|   |-- conditional-access/
|   |-- entra-test-identity/
|   |-- firewall/
|   |-- keyvault/
|   |-- windows-vm/
|   |-- monitoring/
|   |-- networking/
|   |-- policy/
|   `-- sentinel/
|-- policies/
|-- tests/
|-- wiki/
|-- main.tf
|-- variables.tf
|-- outputs.tf
`-- terraform.tfvars.example

References

License

MIT. See LICENSE.

About

Learn Azure security with this hands-on Terraform lab. Deploys a compact hub-spoke environment with Azure Firewall Basic, NSGs, route tables, Key Vault, Log Analytics, Azure Policy, segmentation tests, detection evidence, and CI validation.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors