Secure Azure Infrastructure with Terraform
Provision hardened Azure infrastructure with Terraform and audit it with Python.
Introduction
30 Second Summary
Every day, cloud security teams discover infrastructure that was deployed with the right intentions but the wrong configuration. A single misconfigured firewall rule or a storage bucket left open to the public internet can expose an entire organization.
In this project, you will build a DevSecOps compliance pipeline that provisions secure Azure infrastructure using Terraform, then validates it with a custom Python security scanner.
What You'll Build
A Terraform configuration that deploys hardened Azure resources, paired with a Python script that programmatically audits those resources for security misconfigurations.
By the end of this project, you'll have:
- A Terraform configuration deploying a hardened Azure environment with deny-by-default network rules and least-privilege security groups.
- A Python compliance scanner using the Azure SDK that authenticates to your subscription and audits deployed resources against security best practices.
- Console output showing PASS/FAIL results for each compliance check, including HTTPS enforcement, public access controls, and wildcard NSG rule detection.
- Secret Mission: Extend the scanner with encryption validation and build a toggle for testing misconfiguration detection.
Are there any prerequisites?
This project requires an Azure free account, Terraform installed, Python 3.10+ installed, and the Azure CLI installed and authenticated.
You will also need VS Code as your code editor. Step 1 walks you through verifying each tool and setting up a Python virtual environment.
Set Up Your Project and Install Dependencies
Before writing any Terraform or Python code, you need a clean workspace with the right folder structure and dependencies installed.
In this step, you will create the project directory, confirm your tools are working, set up a Python virtual environment, and install the Azure SDK packages your compliance scanner needs.
In this step, get ready to:
- Create the project folder with separate directories for Terraform and Python code.
- Set up a Python virtual environment and install the Azure SDK packages.
- Authenticate to Azure and verify everything works together.
Create the project directory and verify your tools
Your project needs two main folders. terraform/ holds your infrastructure configuration. scanner/ holds your Python compliance code.
- In VS Code, open a new terminal by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and its subdirectories by running these commands:
mkdir secure-azure-infra
cd secure-azure-infra
mkdir terraform scanner
- Confirm the folders exist by running:
ls
You should see terraform and scanner in the output.
- Open this project folder in VS Code by clicking File in the menu bar, then Open Folder.
- Select the secure-azure-infra folder on your Desktop, then click Open.
- If VS Code asks whether you trust the authors of the files in this folder, click Yes, I trust the authors.
- Open a new terminal again by pressing Ctrl+`.
Now confirm your pre-installed tools are working.
- Verify Terraform is installed by running:
terraform --version
You should see a version number in the output.
- Verify Azure CLI is connected to your account by running:
az account show
You should see JSON output displaying your subscription name and ID.
Seeing 'command not found'?
If terraform --version fails, confirm Terraform is installed and on your system PATH.
If az account show returns an error about not being logged in, you will fix this in Substep 3 when you run az login.
Troubleshoot with AI help
Set up a Python virtual environment and install packages
A virtual environment isolates your project's Python packages from the rest of your system. This prevents version conflicts between projects.
- Create a virtual environment in your project root by running:
python3 -m venv venv
What is a virtual environment?
A virtual environment is an isolated Python installation specific to one project. Packages you install inside it stay separate from your global Python installation.
- Activate the virtual environment by running:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms you are working inside the virtual environment.
Not seeing (venv) in your prompt?
On Windows, you may need to enable script execution first. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then try activating again.
Help me activate my Python virtual environment
Now you will create the requirements file that lists the exact Azure SDK packages your scanner needs.
- In the VS Code Explorer sidebar, right-click the scanner folder and select New File.
- Name the file requirements.txt and press Enter.
- Paste the following package list into requirements.txt:
azure-identity==1.25.3
azure-mgmt-storage==25.0.0
azure-mgmt-network==30.2.0
What are these packages?
- azure-identity handles authentication to Azure using your CLI credentials.
- azure-mgmt-storage lets Python inspect and manage Storage Account properties.
- azure-mgmt-network lets Python inspect Network Security Group rules.
- Save requirements.txt.
- Install the packages by running this command in your terminal:
pip install -r scanner/requirements.txt
You should see output showing each package downloading and installing successfully.
Seeing installation errors?
Make sure (venv) is visible in your terminal prompt. If it is not, re-run the activation command from above before installing.
If you see errors about Python version compatibility, confirm you are running Python 3.10+ with python3 --version.
Help me fix my pip install errors
✔️ Awesome, I've got everything!
Great. Double check you have saved requirements.txt before moving on.
ⓧ I'd like to double check the full code
azure-identity==1.25.3
azure-mgmt-storage==25.0.0
azure-mgmt-network==30.2.0
Authenticate to Azure and verify the SDK imports
Your Python scanner needs to authenticate to Azure. The Azure SDK picks up credentials from your Azure CLI session automatically.
- Log in to Azure by running:
az login
A browser window opens asking you to sign in with your Azure account. After signing in, the terminal displays a JSON list of your subscriptions.
- Find the id field in the output. This is your subscription ID.
- Copy and save your subscription ID somewhere you can easily access it later (a sticky note or separate text file works).
Why do I need the subscription ID?
The Python compliance scanner uses this ID to tell the Azure SDK which subscription's resources to audit. You will set it as an environment variable when you run the scanner in a later step.
Finally, confirm that Python can load the Azure SDK packages you just installed.
- Run this verification command:
python -c "from azure.identity import DefaultAzureCredential; print('OK')"
You should see OK printed in the terminal. This confirms the Azure SDK is installed and importable.
Seeing a ModuleNotFoundError?
This means Python cannot find the Azure packages. Confirm your virtual environment is active by checking for (venv) in the prompt.
If (venv) is showing, try running pip install -r scanner/requirements.txt again and watch for errors.
Help me fix my Azure SDK import error
Your workspace is ready to go. Next up, you will write the Terraform configuration that defines your security-hardened Azure infrastructure.
Write the Terraform Configuration
Your project environment is ready with all dependencies installed and Azure CLI authenticated.
Now it is time to define the infrastructure itself. Terraform uses declarative configuration files to describe what resources you want in the cloud. In this step, you will write four files that together define a security-hardened Azure environment.
In this step, get ready to:
- Create the provider and variable configuration files that pin your Terraform version and define reusable settings.
- Write the main infrastructure file with a Resource Group, hardened Storage Account, and restrictive Network Security Group.
- Create an outputs file that exports resource names for the Python scanner to consume.
Create the provider and variable configuration
Terraform needs to know which cloud provider you are targeting and what version of the provider plugin to use. The provider file locks this down so your configuration is reproducible.
- In VS Code, create a new file called providers.tf inside your terraform/ folder.
- Paste the following code into providers.tf:
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.76"
}
}
}
provider "azurerm" {
features {}
}
What does this code do?
This file configures two things:
- required_version = ">= 1.0" ensures anyone running this project has Terraform 1.0 or later installed.
- version = "~> 4.76" pins the azurerm provider to version 4.76.x. This prevents breaking changes from newer major versions while still receiving patch updates.
- The features {} block is required by the Azure provider even if you do not enable any optional features.
- Save providers.tf.
- Confirm it appears in your terraform/ folder in VS Code's Explorer sidebar.
Next, you need a variables file so your configuration is flexible and reusable. Variables let you change values like region or resource names without editing the main infrastructure code.
- Create a new file called variables.tf inside your terraform/ folder.
- Paste the following code into variables.tf:
variable "location" {
description = "Azure region for all resources"
type = string
default = "East US"
}
variable "resource_group_name" {
description = "Name of the resource group"
type = string
default = "rg-secure-infra"
}
variable "storage_account_name" {
description = "Name of the storage account (must be globally unique, lowercase alphanumeric)"
type = string
default = "secinfrastorage"
}
variable "allowed_cidr" {
description = "CIDR block allowed inbound HTTPS access in the NSG"
type = string
default = "10.0.0.0/16"
}
What does this code do?
Each variable block defines a reusable input with a description, type, and default value:
- location sets the Azure region where all resources will be deployed.
- resource_group_name names the container that groups all your resources together.
- storage_account_name must be globally unique across all of Azure. It only allows lowercase letters and numbers.
- allowed_cidr defines which IP range is allowed to connect via HTTPS in the Network Security Group rules.
Storage account names are globally unique
The default secinfrastorage might already be taken by another Azure user. If you get a naming conflict later during deployment, come back and change this default to something unique like secinfra followed by your initials and a number.
- Save variables.tf.
- Confirm it appears alongside providers.tf in the terraform/ folder.
File not showing in the sidebar?
Make sure you created the file inside the terraform/ subdirectory, not in the project root. If it ended up in the wrong place, drag it into the terraform/ folder in the Explorer sidebar.
help me find where my file was saved
Define the secure infrastructure resources
The main configuration file is where you define the actual cloud resources. You will create three resources that form a security-hardened environment: a Resource Group to contain everything, a Storage Account with locked-down network rules, and a Network Security Group with least-privilege firewall rules.
- Create a new file called main.tf inside your terraform/ folder.
- Paste the following code into main.tf:
resource "azurerm_resource_group" "main" {
name = var.resource_group_name
location = var.location
tags = {
environment = "security-lab"
managed_by = "terraform"
}
}
resource "azurerm_storage_account" "main" {
name = var.storage_account_name
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
https_traffic_only_enabled = true
public_network_access_enabled = false
network_rules {
default_action = "Deny"
bypass = ["AzureServices"]
}
tags = {
environment = "security-lab"
managed_by = "terraform"
}
}
resource "azurerm_network_security_group" "main" {
name = "nsg-secure-infra"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "AllowHTTPSInbound"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = var.allowed_cidr
destination_address_prefix = "VirtualNetwork"
}
security_rule {
name = "DenyAllInbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = {
environment = "security-lab"
managed_by = "terraform"
}
}
What does this code do?
This file defines three Azure resources with security hardening built in:
- azurerm_resource_group is a logical container that groups your resources together for management and cleanup.
- azurerm_storage_account provisions a storage account with three security controls: https_traffic_only_enabled = true blocks unencrypted HTTP connections, public_network_access_enabled = false hides it from the public internet, and default_action = "Deny" in the network rules blocks all traffic unless explicitly allowed.
- azurerm_network_security_group acts as a firewall with two rules. The first rule (priority 100) allows only HTTPS traffic from your specified CIDR range. The second rule (priority 4096) denies everything else.
Lower priority numbers are evaluated first, so the HTTPS allow rule at 100 takes precedence over the deny-all at 4096.
Why deny-by-default networking?
In production, the safest approach is to block all traffic and then open only what is needed. This is called least-privilege networking. If an attacker finds your resource, they cannot reach it unless they come from the exact IP range you allowed on port 443.
- Save main.tf.
- Confirm it appears in the terraform/ folder alongside your other files.
Seeing red squiggles in main.tf?
If VS Code shows syntax warnings, make sure you have the HashiCorp Terraform extension installed. Without it, VS Code does not understand .tf file syntax. The warnings will not affect deployment, but the extension helps catch typos early.
help me fix syntax warnings in my Terraform file
Create the outputs file for the Python scanner
The Python compliance scanner you will build later needs to know the names of your deployed resources. Terraform outputs export values from your configuration so other tools can consume them.
- Create a new file called outputs.tf inside your terraform/ folder.
- Paste the following code into outputs.tf:
output "resource_group_name" {
description = "Name of the deployed resource group"
value = azurerm_resource_group.main.name
}
output "storage_account_name" {
description = "Name of the deployed storage account"
value = azurerm_storage_account.main.name
}
output "storage_account_id" {
description = "ID of the deployed storage account"
value = azurerm_storage_account.main.id
}
output "nsg_name" {
description = "Name of the deployed Network Security Group"
value = azurerm_network_security_group.main.name
}
What does this code do?
Each output block exports a value that Terraform prints after deployment:
- resource_group_name and storage_account_name give the Python scanner the exact names to query.
- storage_account_id provides the full Azure resource ID for detailed API lookups.
- nsg_name identifies the Network Security Group so the scanner can audit its rules.
- Save outputs.tf.
Your terraform/ folder should now contain four files: providers.tf, variables.tf, main.tf, and outputs.tf.
- Validate your entire Terraform configuration by running this command from the terraform/ directory:
cd terraform
terraform validate
- You should see Success! The configuration is valid. printed in the terminal.
Seeing validation errors?
Check for mismatched braces or missing quotes in your .tf files. Terraform error messages point to the exact file and line number where the problem is.
If you see a provider-related error, make sure your providers.tf has the exact source = "hashicorp/azurerm" line.
help me fix my terraform validate errors
✔️ Awesome, I've got everything!
Great. Make sure all four files are saved in your terraform/ folder before moving on.
ⓧ I'd like to double check the full code
Here is each file in full. Compare yours against these:
providers.tf:
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.76"
}
}
}
provider "azurerm" {
features {}
}
variables.tf:
variable "location" {
description = "Azure region for all resources"
type = string
default = "East US"
}
variable "resource_group_name" {
description = "Name of the resource group"
type = string
default = "rg-secure-infra"
}
variable "storage_account_name" {
description = "Name of the storage account (must be globally unique, lowercase alphanumeric)"
type = string
default = "secinfrastorage"
}
variable "allowed_cidr" {
description = "CIDR block allowed inbound HTTPS access in the NSG"
type = string
default = "10.0.0.0/16"
}
main.tf:
resource "azurerm_resource_group" "main" {
name = var.resource_group_name
location = var.location
tags = {
environment = "security-lab"
managed_by = "terraform"
}
}
resource "azurerm_storage_account" "main" {
name = var.storage_account_name
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
https_traffic_only_enabled = true
public_network_access_enabled = false
network_rules {
default_action = "Deny"
bypass = ["AzureServices"]
}
tags = {
environment = "security-lab"
managed_by = "terraform"
}
}
resource "azurerm_network_security_group" "main" {
name = "nsg-secure-infra"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "AllowHTTPSInbound"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = var.allowed_cidr
destination_address_prefix = "VirtualNetwork"
}
security_rule {
name = "DenyAllInbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = {
environment = "security-lab"
managed_by = "terraform"
}
}
outputs.tf:
output "resource_group_name" {
description = "Name of the deployed resource group"
value = azurerm_resource_group.main.name
}
output "storage_account_name" {
description = "Name of the deployed storage account"
value = azurerm_storage_account.main.name
}
output "storage_account_id" {
description = "ID of the deployed storage account"
value = azurerm_storage_account.main.id
}
output "nsg_name" {
description = "Name of the deployed Network Security Group"
value = azurerm_network_security_group.main.name
}
Your infrastructure is fully defined and validated. Next up, you will deploy these resources to Azure and see them running live in your subscription.
Deploy the Secure Infrastructure
Your Terraform configuration is validated and ready to go. Now it's time to bring those security-hardened resources to life in Azure.
In this step, you'll initialize Terraform so it downloads the Azure provider, preview what it plans to create, then deploy all three resources. By the end, you'll have a live, locked-down Azure environment ready for the Python scanner to audit.
In this step, get ready to:
- Initialize the Terraform provider and preview the deployment plan.
- Deploy all three resources to Azure.
- Confirm the exported outputs the Python scanner will consume.
Initialize Terraform and preview the plan
Before Terraform can create anything, it needs to download the Azure provider plugin you specified in providers.tf. This is what terraform init does.
- In your terminal (still in the terraform directory from the previous step), initialize the provider by running this command:
terraform init
What does this command do?
terraform init reads your provider configuration and downloads the azurerm plugin from the Terraform Registry.
It also creates a .terraform directory and a .terraform.lock.hcl lock file to pin the exact provider version for reproducible builds.
- You should see a success message indicating the provider was installed and Terraform is ready.
Seeing a provider error?
- Make sure you're running the command from inside the terraform directory, not the project root.
- Check your internet connection. Terraform needs to download the provider from the registry.
- Verify that providers.tf has the correct source value of hashicorp/azurerm.
Help me troubleshoot my terraform init error
Now that the provider is ready, you can preview exactly what Terraform will create.
- Preview the deployment plan by running:
terraform plan
What does terraform plan show you?
terraform plan reads your .tf files and compares them against the current state (which is empty since you haven't deployed yet).
It produces a detailed preview of every resource it will create, including all attributes and their values. This lets you catch mistakes before any real infrastructure gets provisioned.
- Review the plan output. At the bottom, you should see a summary line: Plan: 3 to add, 0 to change, 0 to destroy.
- Confirm the three resources listed are azurerm_resource_group, azurerm_storage_account, and azurerm_network_security_group.
Plan shows errors instead of resources?
- Run terraform validate first to check for syntax issues in your .tf files.
- Make sure az account show still returns your subscription details. If your session expired, run az login again.
Help me fix my terraform plan errors
Deploy the resources to Azure
The plan looks good. Time to make it real.
- Deploy all three resources by running:
terraform apply -auto-approve
What does -auto-approve do?
Normally terraform apply asks you to type yes to confirm. The -auto-approve flag skips that prompt since you already reviewed the plan in the previous command.
- Wait for the deployment to finish. This typically takes 30-60 seconds.
- You should see a success message: Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
- Verify the resources exist in Azure by running:
az resource list --resource-group rg-secure-infra --output table
- You should see a table listing the Storage Account and Network Security Group in the rg-secure-infra resource group.
Apply failed with a name conflict?
Azure Storage Account names must be globally unique across all of Azure. If secinfrastorage is already taken by another Azure user, the apply will fail.
- Open terraform/variables.tf and change the storage_account_name default to something unique (e.g. add your initials and a number).
- Re-run terraform apply -auto-approve.
Help me fix my terraform apply error
Check the Terraform outputs
Your outputs.tf file exports key values that the Python compliance scanner will need to know which resources to audit.
- Display the exported values by running:
terraform output
Why are outputs important here?
Terraform outputs act as a bridge between your infrastructure and other tools. In the next step, your Python scanner will use these exact resource names and IDs to connect to Azure and audit each resource.
This separation keeps your scanner flexible. It doesn't need to know how resources were created, only where to find them.
- You should see four values printed: resource_group_name, storage_account_name, storage_account_id, and nsg_name.
- Note down the resource_group_name and storage_account_name values. You'll set these as environment variables when you run the Python scanner.
Verify in the Azure portal
You can also confirm your resources visually by signing in to the Azure portal, navigating to **Resource groups**, and selecting rg-secure-infra. You should see all three resources listed there.
Your secure Azure infrastructure is live. Next up, you'll build the Python compliance scanner that audits these resources and reports whether they pass security best practices.
Build the Python Compliance Scanner
Your secure infrastructure is deployed and verified in Azure. Now it's time to build the tool that proves it's actually secure.
In this step, you'll write a Python script that connects to your Azure subscription and programmatically audits each resource against security best practices. This is the same workflow DevSecOps teams use to catch misconfigurations before they reach production.
In this step, get ready to:
- Set up Azure authentication and SDK clients in Python.
- Write three compliance check functions that audit storage and network resources.
- Run the scanner against your deployed infrastructure and confirm all checks pass.
Set up Azure authentication and SDK clients
The scanner needs to authenticate to Azure and create client objects for each service it will audit. DefaultAzureCredential automatically picks up the credentials from your az login session.
- In VS Code, create a new file called compliance_scanner.py inside the scanner/ directory.
- Add the imports and authentication function by pasting this code:
import os
import sys
from azure.identity import DefaultAzureCredential
from azure.mgmt.storage import StorageManagementClient
from azure.mgmt.network import NetworkManagementClient
def get_clients(subscription_id):
# Authenticate using the Azure CLI session from az login
credential = DefaultAzureCredential()
# Create a client for auditing storage accounts
storage_client = StorageManagementClient(
credential=credential, subscription_id=subscription_id
)
# Create a client for auditing network security groups
network_client = NetworkManagementClient(
credential=credential, subscription_id=subscription_id
)
return storage_client, network_client
What does this code do?
The get_clients() function creates two SDK clients that your scanner will use to query Azure resources.
- StorageManagementClient lets you inspect storage account properties like HTTPS enforcement and network rules.
- NetworkManagementClient lets you read NSG rules and check for overly permissive configurations.
- DefaultAzureCredential automatically uses the token from your earlier az login session. No extra setup needed.
- Save compliance_scanner.py.
You won't see output yet. The next chunk adds the compliance checks that these clients feed into.
Write the compliance check functions
Each check function queries a single security property and returns a pass/fail tuple. This pattern keeps each check independent and testable.
- In compliance_scanner.py, add the three check functions below the get_clients() function:
def check_storage_https_only(storage_client, resource_group, account_name):
# Retrieve the storage account properties from Azure
account = storage_client.storage_accounts.get_properties(
resource_group, account_name
)
# Check if HTTPS-only traffic is enforced
if account.enable_https_traffic_only:
return True, "HTTPS-only traffic is enforced"
return False, "HTTPS-only traffic is NOT enforced"
def check_storage_public_access(storage_client, resource_group, account_name):
# Retrieve the storage account properties from Azure
account = storage_client.storage_accounts.get_properties(
resource_group, account_name
)
# Check if public network access is disabled
if account.public_network_access == "Disabled":
return True, "Public network access is disabled"
return False, "Public network access is ENABLED (insecure)"
def check_nsg_no_wildcard_rules(network_client, resource_group, nsg_name):
# Retrieve the NSG and its security rules
nsg = network_client.network_security_groups.get(resource_group, nsg_name)
# Look for any Allow rule that permits all traffic from everywhere
for rule in nsg.security_rules:
if (
rule.access == "Allow"
and rule.source_address_prefix == "*"
and rule.destination_address_prefix == "*"
and rule.destination_port_range == "*"
):
return False, f"Overly permissive rule found: {rule.name}"
return True, "No overly permissive wildcard rules found"
What do these checks validate?
- check_storage_https_only() confirms the storage account rejects unencrypted HTTP traffic. This prevents data from being transmitted in plain text.
- check_storage_public_access() confirms the storage account is not reachable from the public internet. The public_network_access property returns a string ("Disabled" or "Enabled"), not a boolean.
- check_nsg_no_wildcard_rules() loops through every rule in the NSG looking for a dangerous pattern: an Allow rule where source, destination, and port are all wildcards (*). That pattern means "allow everything from everywhere."
Why build a custom scanner instead of using tfsec or checkov?
Tools like tfsec and checkov scan Terraform files statically. Your scanner queries the live Azure environment. This catches drift between what Terraform says and what actually exists.
Building it yourself also teaches you the Azure SDK patterns that power those industry tools under the hood.
- Save compliance_scanner.py.
You won't see output yet. The next chunk adds the audit runner that calls these functions and prints the report.
Write the audit runner and run the scanner
The run_audit() function ties everything together. It runs all three checks, formats the results into a compliance report, and exits with a status code that CI/CD pipelines can act on.
- In compliance_scanner.py, add the audit runner and main block below your check functions:
def run_audit(subscription_id, resource_group, storage_account, nsg_name):
# Initialize both SDK clients
storage_client, network_client = get_clients(subscription_id)
# Run each compliance check and collect results
checks = [
("HTTPS Enforcement", check_storage_https_only(storage_client, resource_group, storage_account)),
("Public Access Disabled", check_storage_public_access(storage_client, resource_group, storage_account)),
("NSG No Wildcard Rules", check_nsg_no_wildcard_rules(network_client, resource_group, nsg_name)),
]
# Print a formatted compliance report
print("\n" + "=" * 60)
print(" AZURE INFRASTRUCTURE COMPLIANCE REPORT")
print("=" * 60 + "\n")
all_passed = True
for check_name, (passed, message) in checks:
status = "PASS" if passed else "FAIL"
icon = "[+]" if passed else "[-]"
print(f" {icon} {status}: {check_name}")
print(f" {message}\n")
if not passed:
all_passed = False
print("=" * 60)
if all_passed:
print(" RESULT: ALL CHECKS PASSED")
else:
print(" RESULT: COMPLIANCE FAILURES DETECTED")
print("=" * 60 + "\n")
return all_passed
if __name__ == "__main__":
# Read configuration from environment variables
subscription_id = os.environ.get("AZURE_SUBSCRIPTION_ID")
resource_group = os.environ.get("AZURE_RESOURCE_GROUP", "rg-secure-infra")
storage_account = os.environ.get("AZURE_STORAGE_ACCOUNT", "secinfrastorage")
nsg_name = os.environ.get("AZURE_NSG_NAME", "nsg-secure-infra")
if not subscription_id:
print("Error: AZURE_SUBSCRIPTION_ID environment variable is required.")
sys.exit(1)
success = run_audit(subscription_id, resource_group, storage_account, nsg_name)
sys.exit(0 if success else 1)
What does this code do?
- run_audit() calls all three check functions, prints a formatted report with PASS/FAIL for each, and returns True only if every check passes.
- The __main__ block reads resource names from environment variables. It defaults to the names your Terraform config created (rg-secure-infra, secinfrastorage, nsg-secure-infra).
- sys.exit(0 if success else 1) returns exit code 0 for success or 1 for failure. CI/CD pipelines use this to gate deployments.
- Save compliance_scanner.py.
- Set the AZURE_SUBSCRIPTION_ID environment variable and run the scanner with this command:
export AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv)
python scanner/compliance_scanner.py
You should see a compliance report with all three checks showing PASS.
What should I see?
Your terminal should display a report like this:
============================================================
AZURE INFRASTRUCTURE COMPLIANCE REPORT
============================================================
[+] PASS: HTTPS Enforcement
HTTPS-only traffic is enforced
[+] PASS: Public Access Disabled
Public network access is disabled
[+] PASS: NSG No Wildcard Rules
No overly permissive wildcard rules found
============================================================
RESULT: ALL CHECKS PASSED
============================================================
Seeing an authentication error?
- Make sure your virtual environment is still active. You should see (venv) or similar prefix in your terminal prompt.
- Run az account show to confirm you're still logged in. If your session expired, run az login again.
- Confirm AZURE_SUBSCRIPTION_ID is set by running echo $AZURE_SUBSCRIPTION_ID. If it's empty, the export command above may have failed.
Help me debug an authentication error running my compliance scanner.
✔️ Awesome, I've got everything!
Great. Double check you've saved compliance_scanner.py.
ⓧ I'd like to double check the full code
import os
import sys
from azure.identity import DefaultAzureCredential
from azure.mgmt.storage import StorageManagementClient
from azure.mgmt.network import NetworkManagementClient
def get_clients(subscription_id):
credential = DefaultAzureCredential()
storage_client = StorageManagementClient(
credential=credential, subscription_id=subscription_id
)
network_client = NetworkManagementClient(
credential=credential, subscription_id=subscription_id
)
return storage_client, network_client
def check_storage_https_only(storage_client, resource_group, account_name):
account = storage_client.storage_accounts.get_properties(
resource_group, account_name
)
if account.enable_https_traffic_only:
return True, "HTTPS-only traffic is enforced"
return False, "HTTPS-only traffic is NOT enforced"
def check_storage_public_access(storage_client, resource_group, account_name):
account = storage_client.storage_accounts.get_properties(
resource_group, account_name
)
if account.public_network_access == "Disabled":
return True, "Public network access is disabled"
return False, "Public network access is ENABLED (insecure)"
def check_nsg_no_wildcard_rules(network_client, resource_group, nsg_name):
nsg = network_client.network_security_groups.get(resource_group, nsg_name)
for rule in nsg.security_rules:
if (
rule.access == "Allow"
and rule.source_address_prefix == "*"
and rule.destination_address_prefix == "*"
and rule.destination_port_range == "*"
):
return False, f"Overly permissive rule found: {rule.name}"
return True, "No overly permissive wildcard rules found"
def run_audit(subscription_id, resource_group, storage_account, nsg_name):
storage_client, network_client = get_clients(subscription_id)
checks = [
("HTTPS Enforcement", check_storage_https_only(storage_client, resource_group, storage_account)),
("Public Access Disabled", check_storage_public_access(storage_client, resource_group, storage_account)),
("NSG No Wildcard Rules", check_nsg_no_wildcard_rules(network_client, resource_group, nsg_name)),
]
print("\n" + "=" * 60)
print(" AZURE INFRASTRUCTURE COMPLIANCE REPORT")
print("=" * 60 + "\n")
all_passed = True
for check_name, (passed, message) in checks:
status = "PASS" if passed else "FAIL"
icon = "[+]" if passed else "[-]"
print(f" {icon} {status}: {check_name}")
print(f" {message}\n")
if not passed:
all_passed = False
print("=" * 60)
if all_passed:
print(" RESULT: ALL CHECKS PASSED")
else:
print(" RESULT: COMPLIANCE FAILURES DETECTED")
print("=" * 60 + "\n")
return all_passed
if __name__ == "__main__":
subscription_id = os.environ.get("AZURE_SUBSCRIPTION_ID")
resource_group = os.environ.get("AZURE_RESOURCE_GROUP", "rg-secure-infra")
storage_account = os.environ.get("AZURE_STORAGE_ACCOUNT", "secinfrastorage")
nsg_name = os.environ.get("AZURE_NSG_NAME", "nsg-secure-infra")
if not subscription_id:
print("Error: AZURE_SUBSCRIPTION_ID environment variable is required.")
sys.exit(1)
success = run_audit(subscription_id, resource_group, storage_account, nsg_name)
sys.exit(0 if success else 1)
Your scanner is working and confirming that the infrastructure meets your security standards. Next up, you'll prove the scanner can catch real problems by deliberately introducing a misconfiguration.
Secret mission
Test the Scanner Catches Misconfigurations
Your scanner works great against hardened infrastructure. But can it actually catch problems? In this mission, you'll add a Terraform toggle that deliberately introduces a security misconfiguration, prove the scanner detects it, then extend the scanner with a fourth compliance check for blob encryption at rest.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
All Azure resources in this project (Resource Group, Storage Account, Network Security Group) are free-tier. There are no ongoing costs, but cleaning up is still good practice.
Resources you used:
- Azure Resource Group (rg-secure-infra).
- Azure Storage Account (secinfrastorage).
- Azure Network Security Group (nsg-secure-infra).
- Local project files (terraform/ and scanner/ directories).
- Python virtual environment.
Keep everything running
No action needed. Choose this if you're still actively building or plan to extend this project further.
- Your Azure resources remain deployed. No charges are expected for this configuration (Resource Groups, NSGs, and Standard/LRS Storage Accounts within free-tier limits are free).
- Your local project files and virtual environment stay on your machine.
- You can re-run the compliance scanner any time with python scanner/compliance_scanner.py.
Pause - I'll come back to this later
Free up local resources while keeping your Azure infrastructure intact so you can return later.
- Deactivate your Python virtual environment by running:
deactivate
- Your Azure resources stay deployed at no cost. When you return, reactivate the virtual environment and pick up where you left off.
Delete - I don't want to use this again
Remove all project resources and start fresh. This is the recommended option if you are done with the project.
- Destroy all Azure resources by running this command from your terraform/ directory:
terraform destroy -auto-approve
- This removes the Resource Group, Storage Account, and Network Security Group from your Azure subscription.
- Deactivate your Python virtual environment:
deactivate
- Navigate to your Desktop and delete the project folder.
macOS
cd ~/Desktop
rm -rf secure-azure-terraform
Windows
cd ~\Desktop
rmdir /s /q secure-azure-terraform
- This removes all local Terraform files, Python scripts, the virtual environment, and the README.
Nice Work!
Nice Work!
You just built a complete DevSecOps compliance pipeline from scratch. Your project provisions secure Azure infrastructure with Terraform and automatically validates it with a custom Python scanner.
You've learned how to:
- Deployed a security-hardened Azure environment using Terraform with proper variable separation, provider pinning, and outputs.
- Built a Python compliance scanner using the Azure SDK that authenticates and audits deployed infrastructure programmatically.
- Validated that the scanner correctly identifies both compliant and non-compliant configurations by toggling security rules.
- Secret Mission: Extended the scanner with encryption validation and built a toggle for testing misconfiguration detection.
Ready to quiz yourself?